Niansuh commited on
Commit
13a9bdf
·
verified ·
1 Parent(s): 45fecec

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +518 -44
main.py CHANGED
@@ -9,7 +9,6 @@ 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
@@ -39,29 +38,26 @@ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
39
  CLEANUP_INTERVAL = 60 # seconds
40
  RATE_LIMIT_WINDOW = 60 # seconds
41
 
42
- async def rate_limiter_per_ip(request: Request):
43
- """
44
- Rate limiter that enforces a limit based on the client's IP address.
45
- """
46
- client_ip = request.client.host
47
- current_time = time.time()
48
 
49
- # Initialize or update the count and timestamp
50
- if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
51
- rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
52
- else:
53
- if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
54
- logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
55
- raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address')
56
- rate_limit_store[client_ip]["count"] += 1
57
 
 
58
  class Blackbox:
59
  label = "Blackbox AI"
60
  url = "https://www.blackbox.ai"
61
  api_endpoint = "https://www.blackbox.ai/api/chat"
62
  working = True
63
  supports_gpt_4 = True
64
- supports_stream = True
65
  supports_system_message = True
66
  supports_message_history = True
67
 
@@ -70,7 +66,7 @@ class Blackbox:
70
  models = [
71
  default_model,
72
  'blackboxai-pro',
73
- *image_models,
74
  "llama-3.1-8b",
75
  'llama-3.1-70b',
76
  'llama-3.1-405b',
@@ -212,9 +208,165 @@ class Blackbox:
212
  proxy: Optional[str] = None,
213
  websearch: bool = False,
214
  **kwargs
215
- ) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  """
217
- Generates a response from Blackbox AI for the /v1/chat/completions endpoint.
218
 
219
  Parameters:
220
  model (str): Model to use for generating responses.
@@ -223,11 +375,10 @@ class Blackbox:
223
  websearch (bool): Enables or disables web search mode.
224
  **kwargs: Additional keyword arguments.
225
 
226
- Returns:
227
- Dict[str, Any]: The response dictionary in the format required by /v1/chat/completions.
228
  """
229
  model = cls.get_model(model)
230
-
231
  chat_id = cls.generate_random_string()
232
  next_action = cls.generate_next_action()
233
  next_router_state_tree = cls.generate_next_router_state_tree()
@@ -304,6 +455,18 @@ class Blackbox:
304
  "userSelectedModel": cls.userSelectedModel.get(model, model)
305
  }
306
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  async with ClientSession(headers=common_headers) as session:
308
  try:
309
  async with session.post(
@@ -316,30 +479,341 @@ class Blackbox:
316
  text = await response_api_chat.text()
317
  cleaned_response = cls.clean_response(text)
318
 
319
- response_data = {
320
- "id": f"chatcmpl-{uuid.uuid4()}",
321
- "object": "chat.completion",
322
- "created": int(datetime.now().timestamp()),
323
- "model": model,
324
- "choices": [
325
- {
326
- "index": 0,
327
- "message": {
328
- "role": "assistant",
329
- "content": cleaned_response
330
- },
331
- "finish_reason": "stop"
332
- }
333
- ],
334
- "usage": {
335
- "prompt_tokens": sum(len(msg['content'].split()) for msg in messages),
336
- "completion_tokens": len(cleaned_response.split()),
337
- "total_tokens": sum(len(msg['content'].split()) for msg in messages) + len(cleaned_response.split())
338
- }
339
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
- return response_data
 
 
 
 
 
 
 
 
342
  except ClientResponseError as e:
343
  error_text = f"Error {e.status}: {e.message}"
344
  try:
345
  error_response = await e.response.text()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
38
  CLEANUP_INTERVAL = 60 # seconds
39
  RATE_LIMIT_WINDOW = 60 # seconds
40
 
41
+ # Define the ImageResponse model (as used in the new Blackbox class)
42
+ class ImageResponseModel(BaseModel):
43
+ images: str # URL of the generated image
44
+ alt: str
 
 
45
 
46
+ # Custom exception for model not working
47
+ class ModelNotWorkingException(Exception):
48
+ def __init__(self, model: str):
49
+ self.model = model
50
+ self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
51
+ super().__init__(self.message)
 
 
52
 
53
+ # Updated Blackbox Class with New Models and Functionality
54
  class Blackbox:
55
  label = "Blackbox AI"
56
  url = "https://www.blackbox.ai"
57
  api_endpoint = "https://www.blackbox.ai/api/chat"
58
  working = True
59
  supports_gpt_4 = True
60
+ supports_stream = True # New attribute for streaming support
61
  supports_system_message = True
62
  supports_message_history = True
63
 
 
66
  models = [
67
  default_model,
68
  'blackboxai-pro',
69
+ *image_models, # Incorporate image models
70
  "llama-3.1-8b",
71
  'llama-3.1-70b',
72
  'llama-3.1-405b',
 
208
  proxy: Optional[str] = None,
209
  websearch: bool = False,
210
  **kwargs
211
+ ) -> Union[str, ImageResponseModel]:
212
+ model = cls.get_model(model)
213
+ chat_id = cls.generate_random_string()
214
+ next_action = cls.generate_next_action()
215
+ next_router_state_tree = cls.generate_next_router_state_tree()
216
+
217
+ agent_mode = cls.agentMode.get(model, {})
218
+ trending_agent_mode = cls.trendingAgentMode.get(model, {})
219
+
220
+ prefix = cls.model_prefixes.get(model, "")
221
+
222
+ formatted_prompt = ""
223
+ for message in messages:
224
+ role = message.get('role', '').capitalize()
225
+ content = message.get('content', '')
226
+ if role and content:
227
+ formatted_prompt += f"{role}: {content}\n"
228
+
229
+ if prefix:
230
+ formatted_prompt = f"{prefix} {formatted_prompt}".strip()
231
+
232
+ referer_path = cls.model_referers.get(model, f"/?model={model}")
233
+ referer_url = f"{cls.url}{referer_path}"
234
+
235
+ common_headers = {
236
+ 'accept': '*/*',
237
+ 'accept-language': 'en-US,en;q=0.9',
238
+ 'cache-control': 'no-cache',
239
+ 'origin': cls.url,
240
+ 'pragma': 'no-cache',
241
+ 'priority': 'u=1, i',
242
+ 'sec-ch-ua': '"Chromium";v="129", "Not=A?Brand";v="8"',
243
+ 'sec-ch-ua-mobile': '?0',
244
+ 'sec-ch-ua-platform': '"Linux"',
245
+ 'sec-fetch-dest': 'empty',
246
+ 'sec-fetch-mode': 'cors',
247
+ 'sec-fetch-site': 'same-origin',
248
+ 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) '
249
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
250
+ 'Chrome/129.0.0.0 Safari/537.36'
251
+ }
252
+
253
+ headers_api_chat = {
254
+ 'Content-Type': 'application/json',
255
+ 'Referer': referer_url
256
+ }
257
+ headers_api_chat_combined = {**common_headers, **headers_api_chat}
258
+
259
+ payload_api_chat = {
260
+ "messages": [
261
+ {
262
+ "id": chat_id,
263
+ "content": formatted_prompt,
264
+ "role": "user"
265
+ }
266
+ ],
267
+ "id": chat_id,
268
+ "previewToken": None,
269
+ "userId": None,
270
+ "codeModelMode": True,
271
+ "agentMode": agent_mode,
272
+ "trendingAgentMode": trending_agent_mode,
273
+ "isMicMode": False,
274
+ "userSystemPrompt": None,
275
+ "maxTokens": 1024,
276
+ "playgroundTopP": 0.9,
277
+ "playgroundTemperature": 0.5,
278
+ "isChromeExt": False,
279
+ "githubToken": None,
280
+ "clickedAnswer2": False,
281
+ "clickedAnswer3": False,
282
+ "clickedForceWebSearch": False,
283
+ "visitFromDelta": False,
284
+ "mobileClient": False,
285
+ "webSearchMode": websearch,
286
+ "userSelectedModel": cls.userSelectedModel.get(model, model)
287
+ }
288
+
289
+ headers_chat = {
290
+ 'Accept': 'text/x-component',
291
+ 'Content-Type': 'text/plain;charset=UTF-8',
292
+ 'Referer': f'{cls.url}/chat/{chat_id}?model={model}',
293
+ 'next-action': next_action,
294
+ 'next-router-state-tree': next_router_state_tree,
295
+ 'next-url': '/'
296
+ }
297
+ headers_chat_combined = {**common_headers, **headers_chat}
298
+
299
+ data_chat = '[]'
300
+
301
+ async with ClientSession(headers=common_headers) as session:
302
+ try:
303
+ async with session.post(
304
+ cls.api_endpoint,
305
+ headers=headers_api_chat_combined,
306
+ json=payload_api_chat,
307
+ proxy=proxy
308
+ ) as response_api_chat:
309
+ response_api_chat.raise_for_status()
310
+ text = await response_api_chat.text()
311
+ cleaned_response = cls.clean_response(text)
312
+
313
+ if model in cls.image_models:
314
+ match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_response)
315
+ if match:
316
+ image_url = match.group(1)
317
+ image_response = ImageResponseModel(images=image_url, alt="Generated Image")
318
+ return image_response
319
+ else:
320
+ return cleaned_response
321
+ else:
322
+ if websearch:
323
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_response, re.DOTALL)
324
+ if match:
325
+ source_part = match.group(1).strip()
326
+ answer_part = cleaned_response[match.end():].strip()
327
+ try:
328
+ sources = json.loads(source_part)
329
+ source_formatted = "**Source:**\n"
330
+ for item in sources:
331
+ title = item.get('title', 'No Title')
332
+ link = item.get('link', '#')
333
+ position = item.get('position', '')
334
+ source_formatted += f"{position}. [{title}]({link})\n"
335
+ final_response = f"{answer_part}\n\n{source_formatted}"
336
+ except json.JSONDecodeError:
337
+ final_response = f"{answer_part}\n\nSource information is unavailable."
338
+ else:
339
+ final_response = cleaned_response
340
+ else:
341
+ if '$~~~$' in cleaned_response:
342
+ final_response = cleaned_response.split('$~~~$')[0].strip()
343
+ else:
344
+ final_response = cleaned_response
345
+
346
+ return final_response
347
+ except ClientResponseError as e:
348
+ error_text = f"Error {e.status}: {e.message}"
349
+ try:
350
+ error_response = await e.response.text()
351
+ cleaned_error = cls.clean_response(error_response)
352
+ error_text += f" - {cleaned_error}"
353
+ except Exception:
354
+ pass
355
+ return error_text
356
+ except Exception as e:
357
+ return f"Unexpected error during /api/chat request: {str(e)}"
358
+
359
+ @classmethod
360
+ async def create_async_generator(
361
+ cls,
362
+ model: str,
363
+ messages: List[Dict[str, str]],
364
+ proxy: Optional[str] = None,
365
+ websearch: bool = False,
366
+ **kwargs
367
+ ) -> AsyncGenerator[Union[str, ImageResponseModel], None]:
368
  """
369
+ Creates an asynchronous generator for streaming responses from Blackbox AI.
370
 
371
  Parameters:
372
  model (str): Model to use for generating responses.
 
375
  websearch (bool): Enables or disables web search mode.
376
  **kwargs: Additional keyword arguments.
377
 
378
+ Yields:
379
+ Union[str, ImageResponseModel]: Segments of the generated response or ImageResponse objects.
380
  """
381
  model = cls.get_model(model)
 
382
  chat_id = cls.generate_random_string()
383
  next_action = cls.generate_next_action()
384
  next_router_state_tree = cls.generate_next_router_state_tree()
 
455
  "userSelectedModel": cls.userSelectedModel.get(model, model)
456
  }
457
 
458
+ headers_chat = {
459
+ 'Accept': 'text/x-component',
460
+ 'Content-Type': 'text/plain;charset=UTF-8',
461
+ 'Referer': f'{cls.url}/chat/{chat_id}?model={model}',
462
+ 'next-action': next_action,
463
+ 'next-router-state-tree': next_router_state_tree,
464
+ 'next-url': '/'
465
+ }
466
+ headers_chat_combined = {**common_headers, **headers_chat}
467
+
468
+ data_chat = '[]'
469
+
470
  async with ClientSession(headers=common_headers) as session:
471
  try:
472
  async with session.post(
 
479
  text = await response_api_chat.text()
480
  cleaned_response = cls.clean_response(text)
481
 
482
+ if model in cls.image_models:
483
+ match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_response)
484
+ if match:
485
+ image_url = match.group(1)
486
+ image_response = ImageResponseModel(images=image_url, alt="Generated Image")
487
+ yield image_response
488
+ else:
489
+ yield cleaned_response
490
+ else:
491
+ if websearch:
492
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_response, re.DOTALL)
493
+ if match:
494
+ source_part = match.group(1).strip()
495
+ answer_part = cleaned_response[match.end():].strip()
496
+ try:
497
+ sources = json.loads(source_part)
498
+ source_formatted = "**Source:**\n"
499
+ for item in sources:
500
+ title = item.get('title', 'No Title')
501
+ link = item.get('link', '#')
502
+ position = item.get('position', '')
503
+ source_formatted += f"{position}. [{title}]({link})\n"
504
+ final_response = f"{answer_part}\n\n{source_formatted}"
505
+ except json.JSONDecodeError:
506
+ final_response = f"{answer_part}\n\nSource information is unavailable."
507
+ else:
508
+ final_response = cleaned_response
509
+ else:
510
+ if '$~~~$' in cleaned_response:
511
+ final_response = cleaned_response.split('$~~~$')[0].strip()
512
+ else:
513
+ final_response = cleaned_response
514
+
515
+ yield final_response
516
+ except ClientResponseError as e:
517
+ error_text = f"Error {e.status}: {e.message}"
518
+ try:
519
+ error_response = await e.response.text()
520
+ cleaned_error = cls.clean_response(error_response)
521
+ error_text += f" - {cleaned_error}"
522
+ except Exception:
523
+ pass
524
+ yield error_text
525
+ except Exception as e:
526
+ yield f"Unexpected error during /api/chat request: {str(e)}"
527
+
528
+ chat_url = f'{cls.url}/chat/{chat_id}?model={model}'
529
 
530
+ try:
531
+ async with session.post(
532
+ chat_url,
533
+ headers=headers_chat_combined,
534
+ data=data_chat,
535
+ proxy=proxy
536
+ ) as response_chat:
537
+ response_chat.raise_for_status()
538
+ pass
539
  except ClientResponseError as e:
540
  error_text = f"Error {e.status}: {e.message}"
541
  try:
542
  error_response = await e.response.text()
543
+ cleaned_error = cls.clean_response(error_response)
544
+ error_text += f" - {cleaned_error}"
545
+ except Exception:
546
+ pass
547
+ yield error_text
548
+ except Exception as e:
549
+ yield f"Unexpected error during /chat/{chat_id} request: {str(e)}"
550
+
551
+ # FastAPI app setup
552
+ app = FastAPI()
553
+
554
+ # Add the cleanup task when the app starts
555
+ @app.on_event("startup")
556
+ async def startup_event():
557
+ asyncio.create_task(cleanup_rate_limit_stores())
558
+ logger.info("Started rate limit store cleanup task.")
559
+
560
+ # Middleware to enhance security and enforce Content-Type for specific endpoints
561
+ @app.middleware("http")
562
+ async def security_middleware(request: Request, call_next):
563
+ client_ip = request.client.host
564
+ # Enforce that POST requests to /v1/chat/completions must have Content-Type: application/json
565
+ if request.method == "POST" and request.url.path == "/v1/chat/completions":
566
+ content_type = request.headers.get("Content-Type")
567
+ if content_type != "application/json":
568
+ logger.warning(f"Invalid Content-Type from IP: {client_ip} for path: {request.url.path}")
569
+ return JSONResponse(
570
+ status_code=400,
571
+ content={
572
+ "error": {
573
+ "message": "Content-Type must be application/json",
574
+ "type": "invalid_request_error",
575
+ "param": None,
576
+ "code": None
577
+ }
578
+ },
579
+ )
580
+ response = await call_next(request)
581
+ return response
582
+
583
+ # Request Models
584
+ class Message(BaseModel):
585
+ role: str
586
+ content: str
587
+
588
+ class ChatRequest(BaseModel):
589
+ model: str
590
+ messages: List[Message]
591
+ temperature: Optional[float] = 1.0
592
+ top_p: Optional[float] = 1.0
593
+ n: Optional[int] = 1
594
+ max_tokens: Optional[int] = None
595
+ presence_penalty: Optional[float] = 0.0
596
+ frequency_penalty: Optional[float] = 0.0
597
+ logit_bias: Optional[Dict[str, float]] = None
598
+ user: Optional[str] = None
599
+
600
+ # Rate Limiter Cleanup Task
601
+ async def cleanup_rate_limit_stores():
602
+ """
603
+ Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
604
+ """
605
+ while True:
606
+ current_time = time.time()
607
+ ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
608
+ for ip in ips_to_delete:
609
+ del rate_limit_store[ip]
610
+ logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
611
+ await asyncio.sleep(CLEANUP_INTERVAL)
612
+
613
+ # Rate Limiter Dependency
614
+ async def rate_limiter_per_ip(request: Request):
615
+ """
616
+ Rate limiter that enforces a limit based on the client's IP address.
617
+ """
618
+ client_ip = request.client.host
619
+ current_time = time.time()
620
+
621
+ # Initialize or update the count and timestamp
622
+ if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
623
+ rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
624
+ else:
625
+ if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
626
+ logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
627
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
628
+ rate_limit_store[client_ip]["count"] += 1
629
+
630
+ # API Key Dependency
631
+ async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
632
+ """
633
+ Dependency to extract and validate the API key from the Authorization header.
634
+ """
635
+ client_ip = request.client.host
636
+ if authorization is None or not authorization.startswith('Bearer '):
637
+ logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
638
+ raise HTTPException(status_code=401, detail='Invalid authorization header format')
639
+ api_key = authorization[7:]
640
+ if api_key not in API_KEYS:
641
+ logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
642
+ raise HTTPException(status_code=401, detail='Invalid API key')
643
+ return api_key
644
+
645
+ # Endpoint: POST /v1/chat/completions
646
+ @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
647
+ async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
648
+ client_ip = req.client.host
649
+ # Redact user messages only for logging purposes
650
+ redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
651
+
652
+ logger.info(f"Received chat completions request from API key: {api_key} | IP: {client_ip} | Model: {request.model} | Messages: {redacted_messages}")
653
+
654
+ try:
655
+ # Validate that the requested model is available
656
+ if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
657
+ logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
658
+ raise HTTPException(status_code=400, detail="Requested model is not available.")
659
+
660
+ # Check if the model is an image generation model
661
+ is_image_model = request.model in Blackbox.image_models
662
+
663
+ # Generate response
664
+ response_content = await Blackbox.generate_response(
665
+ model=request.model,
666
+ messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
667
+ temperature=request.temperature,
668
+ max_tokens=request.max_tokens
669
+ )
670
+
671
+ # If the model is for image generation, handle accordingly
672
+ if is_image_model and isinstance(response_content, ImageResponseModel):
673
+ logger.info(f"Completed image generation for API key: {api_key} | IP: {client_ip}")
674
+ return {
675
+ "id": f"chatcmpl-{uuid.uuid4()}",
676
+ "object": "chat.completion",
677
+ "created": int(datetime.now().timestamp()),
678
+ "model": request.model,
679
+ "choices": [
680
+ {
681
+ "index": 0,
682
+ "message": {
683
+ "role": "assistant",
684
+ "content": response_content.images # Return the image URL
685
+ },
686
+ "finish_reason": "stop"
687
+ }
688
+ ],
689
+ "usage": {
690
+ "prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
691
+ "completion_tokens": len(response_content.images.split()),
692
+ "total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.images.split())
693
+ },
694
+ }
695
+
696
+ logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
697
+ return {
698
+ "id": f"chatcmpl-{uuid.uuid4()}",
699
+ "object": "chat.completion",
700
+ "created": int(datetime.now().timestamp()),
701
+ "model": request.model,
702
+ "choices": [
703
+ {
704
+ "index": 0,
705
+ "message": {
706
+ "role": "assistant",
707
+ "content": response_content
708
+ },
709
+ "finish_reason": "stop"
710
+ }
711
+ ],
712
+ "usage": {
713
+ "prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
714
+ "completion_tokens": len(response_content.split()),
715
+ "total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.split())
716
+ },
717
+ }
718
+ except ModelNotWorkingException as e:
719
+ logger.warning(f"Model not working: {e} | IP: {client_ip}")
720
+ raise HTTPException(status_code=503, detail=str(e))
721
+ except HTTPException as he:
722
+ logger.warning(f"HTTPException: {he.detail} | IP: {client_ip}")
723
+ raise he
724
+ except Exception as e:
725
+ logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
726
+ raise HTTPException(status_code=500, detail=str(e))
727
+
728
+ # Optional: Endpoint for Streaming Responses (Requires Client Support)
729
+ # If you wish to support streaming, you can implement an endpoint that leverages the asynchronous generator.
730
+ # This requires clients to handle streaming responses appropriately.
731
+
732
+ @app.post("/v1/chat/completions/stream", dependencies=[Depends(rate_limiter_per_ip)])
733
+ async def chat_completions_stream(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
734
+ client_ip = req.client.host
735
+ redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
736
+
737
+ logger.info(f"Received streaming chat completions request from API key: {api_key} | IP: {client_ip} | Model: {request.model} | Messages: {redacted_messages}")
738
+
739
+ try:
740
+ # Validate that the requested model is available
741
+ if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
742
+ logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
743
+ raise HTTPException(status_code=400, detail="Requested model is not available.")
744
+
745
+ # Check if the model is an image generation model
746
+ is_image_model = request.model in Blackbox.image_models
747
+
748
+ # Create an asynchronous generator
749
+ async_gen = Blackbox.create_async_generator(
750
+ model=request.model,
751
+ messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
752
+ temperature=request.temperature,
753
+ max_tokens=request.max_tokens
754
+ )
755
+
756
+ async def stream_response() -> AsyncGenerator[bytes, None]:
757
+ async for chunk in async_gen:
758
+ if isinstance(chunk, ImageResponseModel):
759
+ # For image responses, you might want to send the URL directly
760
+ yield json.dumps({
761
+ "role": "assistant",
762
+ "content": chunk.images
763
+ }).encode('utf-8') + b'\n'
764
+ else:
765
+ yield json.dumps({
766
+ "role": "assistant",
767
+ "content": chunk
768
+ }).encode('utf-8') + b'\n'
769
+
770
+ logger.info(f"Streaming response started for API key: {api_key} | IP: {client_ip}")
771
+ return JSONResponse(
772
+ content=None, # The actual streaming is handled by the generator
773
+ media_type='text/event-stream',
774
+ background=stream_response()
775
+ )
776
+ except ModelNotWorkingException as e:
777
+ logger.warning(f"Model not working: {e} | IP: {client_ip}")
778
+ raise HTTPException(status_code=503, detail=str(e))
779
+ except HTTPException as he:
780
+ logger.warning(f"HTTPException: {he.detail} | IP: {client_ip}")
781
+ raise he
782
+ except Exception as e:
783
+ logger.exception(f"An unexpected error occurred while processing the streaming chat completions request from IP: {client_ip}.")
784
+ raise HTTPException(status_code=500, detail=str(e))
785
+
786
+ # Endpoint: GET /v1/models
787
+ @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
788
+ async def get_models(req: Request):
789
+ client_ip = req.client.host
790
+ logger.info(f"Fetching available models from IP: {client_ip}")
791
+ return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
792
+
793
+ # Endpoint: GET /v1/health
794
+ @app.get("/v1/health", dependencies=[Depends(rate_limiter_per_ip)])
795
+ async def health_check(req: Request):
796
+ client_ip = req.client.host
797
+ logger.info(f"Health check requested from IP: {client_ip}")
798
+ return {"status": "ok"}
799
+
800
+ # Custom exception handler to match OpenAI's error format
801
+ @app.exception_handler(HTTPException)
802
+ async def http_exception_handler(request: Request, exc: HTTPException):
803
+ client_ip = request.client.host
804
+ logger.error(f"HTTPException: {exc.detail} | Path: {request.url.path} | IP: {client_ip}")
805
+ return JSONResponse(
806
+ status_code=exc.status_code,
807
+ content={
808
+ "error": {
809
+ "message": exc.detail,
810
+ "type": "invalid_request_error",
811
+ "param": None,
812
+ "code": None
813
+ }
814
+ },
815
+ )
816
+
817
+ if __name__ == "__main__":
818
+ import uvicorn
819
+ uvicorn.run(app, host="0.0.0.0", port=8000)