Niansuh commited on
Commit
db33061
·
verified ·
1 Parent(s): f8ac543

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +146 -165
main.py CHANGED
@@ -1,18 +1,19 @@
1
  import os
2
  import re
 
 
3
  import uuid
4
  import json
5
  import logging
6
  import asyncio
7
  import time
8
  from collections import defaultdict
9
- from typing import List, Dict, Any, Optional, AsyncGenerator
10
-
11
  from datetime import datetime
12
 
13
  from aiohttp import ClientSession, ClientTimeout, ClientError
14
  from fastapi import FastAPI, HTTPException, Request, Depends, Header
15
- from fastapi.responses import StreamingResponse
16
  from pydantic import BaseModel
17
 
18
  # Configure logging
@@ -25,26 +26,48 @@ logger = logging.getLogger(__name__)
25
 
26
  # Load environment variables
27
  API_KEYS = os.getenv('API_KEYS', '').split(',') # Comma-separated API keys
28
- RATE_LIMIT_PER_MINUTE = int(os.getenv('RATE_LIMIT_PER_MINUTE', '60')) # Requests per minute per IP
29
 
30
  if not API_KEYS or API_KEYS == ['']:
31
  logger.error("No API keys found. Please set the API_KEYS environment variable.")
32
  raise Exception("API_KEYS environment variable not set.")
33
 
34
- # Simple in-memory rate limiter per IP
35
- rate_limit_store_ip = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- async def rate_limiter(request: Request):
38
- client_host = request.client.host
39
  current_time = time.time()
40
- window_start = rate_limit_store_ip[client_host]["timestamp"]
 
41
  if current_time - window_start > 60:
42
- rate_limit_store_ip[client_host] = {"count": 1, "timestamp": current_time}
43
  else:
44
- if rate_limit_store_ip[client_host]["count"] >= RATE_LIMIT_PER_MINUTE:
45
- logger.warning(f"Rate limit exceeded for IP: {client_host}")
46
- raise HTTPException(status_code=429, detail='Rate limit exceeded.')
47
- rate_limit_store_ip[client_host]["count"] += 1
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  # Custom exception for model not working
50
  class ModelNotWorkingException(Exception):
@@ -195,7 +218,7 @@ class Blackbox:
195
  if not cls.working or model not in cls.models:
196
  logger.error(f"Model {model} is not working or not supported.")
197
  raise ModelNotWorkingException(model)
198
-
199
  headers = {
200
  "accept": "*/*",
201
  "accept-language": "en-US,en;q=0.9",
@@ -211,7 +234,7 @@ class Blackbox:
211
  "sec-fetch-dest": "empty",
212
  "sec-fetch-mode": "cors",
213
  "sec-fetch-site": "same-origin",
214
- "user-agent": "Mozilla/5.0 (X11; Linux x86_64)",
215
  }
216
 
217
  if model in cls.model_prefixes:
@@ -219,7 +242,7 @@ class Blackbox:
219
  if not messages[0]['content'].startswith(prefix):
220
  logger.debug(f"Adding prefix '{prefix}' to the first message.")
221
  messages[0]['content'] = f"{prefix} {messages[0]['content']}"
222
-
223
  random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
224
  messages[-1]['id'] = random_id
225
  messages[-1]['role'] = 'user'
@@ -235,7 +258,7 @@ class Blackbox:
235
  }
236
  messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
237
  logger.debug("Image data added to the message.")
238
-
239
  data = {
240
  "messages": messages,
241
  "id": random_id,
@@ -268,8 +291,8 @@ class Blackbox:
268
  data["userSelectedModel"] = cls.userSelectedModel[model]
269
  logger.info(f"Sending request to {cls.api_endpoint} with data (excluding messages).")
270
 
271
- timeout = ClientTimeout(total=30) # Reduced timeout for faster response
272
- retry_attempts = 3 # Reduced retry attempts for faster failure handling
273
 
274
  for attempt in range(retry_attempts):
275
  try:
@@ -335,204 +358,162 @@ class Blackbox:
335
  # FastAPI app setup
336
  app = FastAPI()
337
 
338
- # Implement per-IP rate limiting middleware
339
- @app.middleware("http")
340
- async def rate_limit_middleware(request: Request, call_next):
341
- await rate_limiter(request)
342
- response = await call_next(request)
343
- return response
344
-
345
- # Pydantic models for OpenAI API
346
  class Message(BaseModel):
347
  role: str
348
  content: str
349
 
350
- class ChatCompletionRequest(BaseModel):
351
  model: str
352
  messages: List[Message]
353
  temperature: Optional[float] = 1.0
354
  top_p: Optional[float] = 1.0
355
  n: Optional[int] = 1
356
  stream: Optional[bool] = False
357
- stop: Optional[Any] = None # Can be a string or list of strings
358
  max_tokens: Optional[int] = None
359
  presence_penalty: Optional[float] = 0.0
360
  frequency_penalty: Optional[float] = 0.0
361
  logit_bias: Optional[Dict[str, float]] = None
362
  user: Optional[str] = None
 
363
 
364
- def create_chat_completion_response(content: str, model: str, usage: Dict[str, int]) -> Dict[str, Any]:
365
  return {
366
  "id": f"chatcmpl-{uuid.uuid4()}",
367
- "object": "chat.completion",
368
  "created": int(datetime.now().timestamp()),
369
  "model": model,
370
  "choices": [
371
  {
372
  "index": 0,
373
- "message": {
374
- "role": "assistant",
375
- "content": content
376
- },
377
- "finish_reason": "stop"
378
  }
379
  ],
380
- "usage": usage
381
  }
382
 
383
- def create_stream_response_chunk(content: str, role: Optional[str] = None, finish_reason: Optional[str] = None):
384
- delta = {}
385
- if role:
386
- delta['role'] = role
387
- if content:
388
- delta['content'] = content
389
- return {
390
- "object": "chat.completion.chunk",
391
- "created": int(datetime.now().timestamp()),
392
- "model": "", # Model name can be added if necessary
393
- "choices": [
394
- {
395
- "delta": delta,
396
- "index": 0,
397
- "finish_reason": finish_reason
398
- }
399
- ]
400
- }
401
 
402
- @app.post("/v1/chat/completions")
403
- async def chat_completions(request: ChatCompletionRequest, authorization: str = Header(None)):
404
- # Verify API key
405
- if not authorization or not authorization.startswith('Bearer '):
406
- logger.warning("Invalid authorization header format.")
407
- raise HTTPException(status_code=401, detail='Invalid authorization header format.')
408
- api_key = authorization[7:]
409
- if api_key not in API_KEYS:
410
- logger.warning(f"Invalid API key attempted: {api_key}")
411
- raise HTTPException(status_code=401, detail='Invalid API key.')
412
-
413
- logger.info(f"Received chat completion request for model: {request.model}")
414
 
415
- # Validate model
416
- if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
417
- logger.warning(f"Attempt to use unavailable model: {request.model}")
418
- raise HTTPException(status_code=400, detail="The model is not available.")
419
-
420
- # Process the request
421
  try:
422
- # Convert messages to dicts
423
- messages = [msg.dict() for msg in request.messages]
424
-
425
- # Check if the user is requesting image generation
426
- image_generation_requested = any(
427
- re.search(r'\b(generate|create|draw)\b.*\b(image|picture|art)\b', msg['content'], re.IGNORECASE)
428
- for msg in messages if msg['role'] == 'user'
 
 
 
 
 
429
  )
430
 
431
- if image_generation_requested:
432
- model = 'ImageGeneration'
433
- # For image generation, use the last message as prompt
434
- prompt = messages[-1]['content']
435
- # Build messages for the Blackbox.create_async_generator
436
- messages = [{"role": "user", "content": prompt}]
437
- async_generator = Blackbox.create_async_generator(
438
- model=model,
439
- messages=messages,
440
- image=None,
441
- image_name=None,
442
- webSearchMode=False
443
- )
444
-
445
- # Collect images
446
- images = []
447
- count = 0
448
- async for response in async_generator:
449
- if isinstance(response, ImageResponse):
450
- images.append(response.url)
451
- count += 1
452
- if count >= request.n:
453
- break
454
-
455
- # Build response content with image URLs
456
- response_content = "\n".join(f"![Generated Image]({url})" for url in images)
457
- completion_tokens = len(response_content.split())
458
- else:
459
- # Use the requested model
460
- async_generator = Blackbox.create_async_generator(
461
- model=request.model,
462
- messages=messages,
463
- image=None,
464
- image_name=None,
465
- webSearchMode=False
466
- )
467
- # Usage tracking
468
- completion_tokens = 0 # Will be updated as we process the response
469
-
470
- prompt_tokens = sum(len(msg['content'].split()) for msg in messages)
471
-
472
  if request.stream:
473
  async def generate():
474
- nonlocal completion_tokens
475
  try:
476
- # Initial delta with role
477
- initial_chunk = create_stream_response_chunk(content=None, role="assistant")
478
- yield f"data: {json.dumps(initial_chunk)}\n\n"
479
-
480
  async for chunk in async_generator:
481
- if isinstance(chunk, str):
482
- completion_tokens += len(chunk.split())
483
- response_chunk = create_stream_response_chunk(content=chunk)
484
- yield f"data: {json.dumps(response_chunk)}\n\n"
485
- elif isinstance(chunk, ImageResponse):
486
- content = f"![Generated Image]({chunk.url})"
487
- completion_tokens += len(content.split())
488
- response_chunk = create_stream_response_chunk(content=content)
489
- yield f"data: {json.dumps(response_chunk)}\n\n"
490
  else:
491
- pass # Handle other types if necessary
492
- # Finish reason
493
- final_chunk = create_stream_response_chunk(content=None, finish_reason="stop")
494
- yield f"data: {json.dumps(final_chunk)}\n\n"
495
  yield "data: [DONE]\n\n"
 
 
 
496
  except Exception as e:
497
  logger.exception("Error during streaming response generation.")
498
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
 
 
499
  return StreamingResponse(generate(), media_type="text/event-stream")
500
  else:
501
  response_content = ""
502
  async for chunk in async_generator:
503
- if isinstance(chunk, str):
 
 
504
  response_content += chunk
505
- elif isinstance(chunk, ImageResponse):
506
- response_content += f"![Generated Image]({chunk.url})\n"
507
- completion_tokens = len(response_content.split())
508
- usage = {
509
- "prompt_tokens": prompt_tokens,
510
- "completion_tokens": completion_tokens,
511
- "total_tokens": prompt_tokens + completion_tokens
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  }
513
- return create_chat_completion_response(response_content, request.model, usage)
514
  except ModelNotWorkingException as e:
515
  logger.warning(f"Model not working: {e}")
516
  raise HTTPException(status_code=503, detail=str(e))
 
 
 
517
  except Exception as e:
518
  logger.exception("An unexpected error occurred while processing the chat completions request.")
519
  raise HTTPException(status_code=500, detail=str(e))
520
 
521
- @app.get("/v1/models")
522
- async def get_models(authorization: str = Header(None)):
523
- # Verify API key
524
- if not authorization or not authorization.startswith('Bearer '):
525
- logger.warning("Invalid authorization header format.")
526
- raise HTTPException(status_code=401, detail='Invalid authorization header format.')
527
- api_key = authorization[7:]
528
- if api_key not in API_KEYS:
529
- logger.warning(f"Invalid API key attempted: {api_key}")
530
- raise HTTPException(status_code=401, detail='Invalid API key.')
531
-
532
- logger.info("Fetching available models.")
533
- # Return models in OpenAI format
534
- models_data = [{"id": model, "object": "model", "owned_by": "organization-owner", "permission": []} for model in Blackbox.models]
535
- return {"data": models_data, "object": "list"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  if __name__ == "__main__":
538
  import uvicorn
 
1
  import os
2
  import re
3
+ import random
4
+ import string
5
  import uuid
6
  import json
7
  import logging
8
  import asyncio
9
  import time
10
  from collections import defaultdict
11
+ from typing import List, Dict, Any, Optional, AsyncGenerator, Union
 
12
  from datetime import datetime
13
 
14
  from aiohttp import ClientSession, ClientTimeout, ClientError
15
  from fastapi import FastAPI, HTTPException, Request, Depends, Header
16
+ from fastapi.responses import StreamingResponse, JSONResponse
17
  from pydantic import BaseModel
18
 
19
  # Configure logging
 
26
 
27
  # Load environment variables
28
  API_KEYS = os.getenv('API_KEYS', '').split(',') # Comma-separated API keys
29
+ RATE_LIMIT = int(os.getenv('RATE_LIMIT', '60')) # Requests per minute
30
 
31
  if not API_KEYS or API_KEYS == ['']:
32
  logger.error("No API keys found. Please set the API_KEYS environment variable.")
33
  raise Exception("API_KEYS environment variable not set.")
34
 
35
+ # Simple in-memory rate limiter
36
+ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
37
+ ip_rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
38
+
39
+ async def get_api_key(authorization: str = Header(...)) -> str:
40
+ if not authorization.startswith('Bearer '):
41
+ logger.warning("Invalid authorization header format.")
42
+ raise HTTPException(status_code=401, detail='Invalid authorization header format')
43
+ api_key = authorization[7:]
44
+ if api_key not in API_KEYS:
45
+ logger.warning(f"Invalid API key attempted: {api_key}")
46
+ raise HTTPException(status_code=401, detail='Invalid API key')
47
+ return api_key
48
 
49
+ async def rate_limiter(req: Request, api_key: str = Depends(get_api_key)):
 
50
  current_time = time.time()
51
+ # Rate limiting per API key
52
+ window_start = rate_limit_store[api_key]["timestamp"]
53
  if current_time - window_start > 60:
54
+ rate_limit_store[api_key] = {"count": 1, "timestamp": current_time}
55
  else:
56
+ if rate_limit_store[api_key]["count"] >= RATE_LIMIT:
57
+ logger.warning(f"Rate limit exceeded for API key: {api_key}")
58
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for API key')
59
+ rate_limit_store[api_key]["count"] += 1
60
+
61
+ # Rate limiting per IP address
62
+ client_ip = req.client.host
63
+ window_start_ip = ip_rate_limit_store[client_ip]["timestamp"]
64
+ if current_time - window_start_ip > 60:
65
+ ip_rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
66
+ else:
67
+ if ip_rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
68
+ logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
69
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address')
70
+ ip_rate_limit_store[client_ip]["count"] += 1
71
 
72
  # Custom exception for model not working
73
  class ModelNotWorkingException(Exception):
 
218
  if not cls.working or model not in cls.models:
219
  logger.error(f"Model {model} is not working or not supported.")
220
  raise ModelNotWorkingException(model)
221
+
222
  headers = {
223
  "accept": "*/*",
224
  "accept-language": "en-US,en;q=0.9",
 
234
  "sec-fetch-dest": "empty",
235
  "sec-fetch-mode": "cors",
236
  "sec-fetch-site": "same-origin",
237
+ "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
238
  }
239
 
240
  if model in cls.model_prefixes:
 
242
  if not messages[0]['content'].startswith(prefix):
243
  logger.debug(f"Adding prefix '{prefix}' to the first message.")
244
  messages[0]['content'] = f"{prefix} {messages[0]['content']}"
245
+
246
  random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
247
  messages[-1]['id'] = random_id
248
  messages[-1]['role'] = 'user'
 
258
  }
259
  messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
260
  logger.debug("Image data added to the message.")
261
+
262
  data = {
263
  "messages": messages,
264
  "id": random_id,
 
291
  data["userSelectedModel"] = cls.userSelectedModel[model]
292
  logger.info(f"Sending request to {cls.api_endpoint} with data (excluding messages).")
293
 
294
+ timeout = ClientTimeout(total=60) # Set an appropriate timeout
295
+ retry_attempts = 10 # Set the number of retry attempts
296
 
297
  for attempt in range(retry_attempts):
298
  try:
 
358
  # FastAPI app setup
359
  app = FastAPI()
360
 
 
 
 
 
 
 
 
 
361
  class Message(BaseModel):
362
  role: str
363
  content: str
364
 
365
+ class ChatRequest(BaseModel):
366
  model: str
367
  messages: List[Message]
368
  temperature: Optional[float] = 1.0
369
  top_p: Optional[float] = 1.0
370
  n: Optional[int] = 1
371
  stream: Optional[bool] = False
372
+ stop: Optional[Union[str, List[str]]] = None
373
  max_tokens: Optional[int] = None
374
  presence_penalty: Optional[float] = 0.0
375
  frequency_penalty: Optional[float] = 0.0
376
  logit_bias: Optional[Dict[str, float]] = None
377
  user: Optional[str] = None
378
+ webSearchMode: Optional[bool] = False # Custom parameter
379
 
380
+ def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
381
  return {
382
  "id": f"chatcmpl-{uuid.uuid4()}",
383
+ "object": "chat.completion.chunk",
384
  "created": int(datetime.now().timestamp()),
385
  "model": model,
386
  "choices": [
387
  {
388
  "index": 0,
389
+ "delta": {"content": content, "role": "assistant"},
390
+ "finish_reason": finish_reason,
 
 
 
391
  }
392
  ],
393
+ "usage": None,
394
  }
395
 
396
+ @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter)])
397
+ async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
398
+ # Redact user messages only for logging purposes
399
+ redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
+ logger.info(f"Received chat completions request from API key: {api_key} | Model: {request.model} | Messages: {redacted_messages}")
 
 
 
 
 
 
 
 
 
 
 
402
 
 
 
 
 
 
 
403
  try:
404
+ # Validate that the requested model is available
405
+ if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
406
+ logger.warning(f"Attempt to use unavailable model: {request.model}")
407
+ raise HTTPException(status_code=400, detail="Requested model is not available.")
408
+
409
+ # Process the request with actual message content, but don't log it
410
+ async_generator = Blackbox.create_async_generator(
411
+ model=request.model,
412
+ messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
413
+ image=None,
414
+ image_name=None,
415
+ webSearchMode=request.webSearchMode
416
  )
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  if request.stream:
419
  async def generate():
 
420
  try:
 
 
 
 
421
  async for chunk in async_generator:
422
+ if isinstance(chunk, ImageResponse):
423
+ image_markdown = f"![image]({chunk.url})"
424
+ response_chunk = create_response(image_markdown, request.model)
 
 
 
 
 
 
425
  else:
426
+ response_chunk = create_response(chunk, request.model)
427
+
428
+ yield f"data: {json.dumps(response_chunk)}\n\n"
429
+
430
  yield "data: [DONE]\n\n"
431
+ except HTTPException as he:
432
+ error_response = {"error": he.detail}
433
+ yield f"data: {json.dumps(error_response)}\n\n"
434
  except Exception as e:
435
  logger.exception("Error during streaming response generation.")
436
+ error_response = {"error": str(e)}
437
+ yield f"data: {json.dumps(error_response)}\n\n"
438
+
439
  return StreamingResponse(generate(), media_type="text/event-stream")
440
  else:
441
  response_content = ""
442
  async for chunk in async_generator:
443
+ if isinstance(chunk, ImageResponse):
444
+ response_content += f"![image]({chunk.url})\n"
445
+ else:
446
  response_content += chunk
447
+
448
+ logger.info(f"Completed non-streaming response generation for API key: {api_key}")
449
+ return {
450
+ "id": f"chatcmpl-{uuid.uuid4()}",
451
+ "object": "chat.completion",
452
+ "created": int(datetime.now().timestamp()),
453
+ "model": request.model,
454
+ "choices": [
455
+ {
456
+ "message": {
457
+ "role": "assistant",
458
+ "content": response_content
459
+ },
460
+ "finish_reason": "stop",
461
+ "index": 0
462
+ }
463
+ ],
464
+ "usage": {
465
+ "prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
466
+ "completion_tokens": len(response_content.split()),
467
+ "total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.split())
468
+ },
469
  }
 
470
  except ModelNotWorkingException as e:
471
  logger.warning(f"Model not working: {e}")
472
  raise HTTPException(status_code=503, detail=str(e))
473
+ except HTTPException as he:
474
+ logger.warning(f"HTTPException: {he.detail}")
475
+ raise he
476
  except Exception as e:
477
  logger.exception("An unexpected error occurred while processing the chat completions request.")
478
  raise HTTPException(status_code=500, detail=str(e))
479
 
480
+ @app.get("/v1/models", dependencies=[Depends(rate_limiter)])
481
+ async def get_models(api_key: str = Depends(get_api_key)):
482
+ logger.info(f"Fetching available models for API key: {api_key}")
483
+ return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
484
+
485
+ # Additional endpoints for better functionality
486
+ @app.get("/v1/health", dependencies=[Depends(rate_limiter)])
487
+ async def health_check(api_key: str = Depends(get_api_key)):
488
+ logger.info(f"Health check requested by API key: {api_key}")
489
+ return {"status": "ok"}
490
+
491
+ @app.get("/v1/models/{model}/status", dependencies=[Depends(rate_limiter)])
492
+ async def model_status(model: str, api_key: str = Depends(get_api_key)):
493
+ logger.info(f"Model status requested for '{model}' by API key: {api_key}")
494
+ if model in Blackbox.models:
495
+ return {"model": model, "status": "available"}
496
+ elif model in Blackbox.model_aliases:
497
+ actual_model = Blackbox.model_aliases[model]
498
+ return {"model": actual_model, "status": "available via alias"}
499
+ else:
500
+ logger.warning(f"Model not found: {model}")
501
+ raise HTTPException(status_code=404, detail="Model not found")
502
+
503
+ # Custom exception handler to match OpenAI's error format
504
+ @app.exception_handler(HTTPException)
505
+ async def http_exception_handler(request: Request, exc: HTTPException):
506
+ return JSONResponse(
507
+ status_code=exc.status_code,
508
+ content={
509
+ "error": {
510
+ "message": exc.detail,
511
+ "type": "invalid_request_error",
512
+ "param": None,
513
+ "code": None
514
+ }
515
+ },
516
+ )
517
 
518
  if __name__ == "__main__":
519
  import uvicorn