Niansuh commited on
Commit
3908754
·
verified ·
1 Parent(s): 87704ef

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +160 -57
main.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import re
2
  import random
3
  import string
@@ -5,12 +6,22 @@ import uuid
5
  import json
6
  import logging
7
  import asyncio
 
8
  from aiohttp import ClientSession, ClientTimeout, ClientError
9
- from fastapi import FastAPI, HTTPException, Request
10
- from pydantic import BaseModel
 
 
11
  from typing import List, Dict, Any, Optional, AsyncGenerator
12
  from datetime import datetime
13
- from fastapi.responses import StreamingResponse
 
 
 
 
 
 
 
14
 
15
  # Configure logging
16
  logging.basicConfig(
@@ -22,6 +33,57 @@ logging.basicConfig(
22
  )
23
  logger = logging.getLogger(__name__)
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # Custom exception for model not working
26
  class ModelNotWorkingException(Exception):
27
  def __init__(self, model: str):
@@ -29,18 +91,34 @@ class ModelNotWorkingException(Exception):
29
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
30
  super().__init__(self.message)
31
 
32
- # Mock implementations for ImageResponse and to_data_uri
33
  class ImageResponse:
34
  def __init__(self, url: str, alt: str):
35
  self.url = url
36
  self.alt = alt
37
 
38
  def to_data_uri(image: Any) -> str:
39
- return "data:image/png;base64,..." # Replace with actual base64 data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
 
41
  class Blackbox:
42
  url = "https://www.blackbox.ai"
43
- api_endpoint = "https://www.blackbox.ai/api/chat"
44
  working = True
45
  supports_stream = True
46
  supports_system_message = True
@@ -100,13 +178,13 @@ class Blackbox:
100
  'XcodeAgent': {'mode': True, 'id': "Xcode Agent"},
101
  'AngularJSAgent': {'mode': True, 'id': "AngularJS Agent"},
102
  }
103
-
104
  userSelectedModel = {
105
  "gpt-4o": "gpt-4o",
106
  "gemini-pro": "gemini-pro",
107
  'claude-sonnet-3.5': "claude-sonnet-3.5",
108
  }
109
-
110
  model_prefixes = {
111
  'gpt-4o': '@GPT-4o',
112
  'gemini-pro': '@Gemini-PRO',
@@ -128,14 +206,14 @@ class Blackbox:
128
  'ImageGeneration': '@Image Generation',
129
  'Niansuh': '@Niansuh',
130
  }
131
-
132
  model_referers = {
133
  "blackboxai": f"{url}/?model=blackboxai",
134
  "gpt-4o": f"{url}/?model=gpt-4o",
135
  "gemini-pro": f"{url}/?model=gemini-pro",
136
  "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
137
  }
138
-
139
  model_aliases = {
140
  "gemini-flash": "gemini-1.5-flash",
141
  "claude-3.5-sonnet": "claude-sonnet-3.5",
@@ -218,7 +296,7 @@ class Blackbox:
218
  "trendingAgentMode": {},
219
  "isMicMode": False,
220
  "userSystemPrompt": None,
221
- "maxTokens": 99999999,
222
  "playgroundTopP": 0.9,
223
  "playgroundTemperature": 0.5,
224
  "isChromeExt": False,
@@ -294,29 +372,41 @@ class Blackbox:
294
  except ClientError as ce:
295
  logger.error(f"Client error occurred: {ce}. Retrying attempt {attempt + 1}/{retry_attempts}")
296
  if attempt == retry_attempts - 1:
297
- raise HTTPException(status_code=502, detail="Error communicating with the external API. | NiansuhAI")
298
  except asyncio.TimeoutError:
299
  logger.error(f"Request timed out. Retrying attempt {attempt + 1}/{retry_attempts}")
300
  if attempt == retry_attempts - 1:
301
- raise HTTPException(status_code=504, detail="External API request timed out. | NiansuhAI")
302
  except Exception as e:
303
  logger.error(f"Unexpected error: {e}. Retrying attempt {attempt + 1}/{retry_attempts}")
304
  if attempt == retry_attempts - 1:
305
  raise HTTPException(status_code=500, detail=str(e))
306
 
307
- # FastAPI app setup
308
- app = FastAPI()
309
-
310
  class Message(BaseModel):
311
- role: str
312
- content: str
313
 
314
  class ChatRequest(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  model: str
316
- messages: List[Message]
317
- stream: Optional[bool] = False
318
- webSearchMode: Optional[bool] = False
319
 
 
320
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
321
  return {
322
  "id": f"chatcmpl-{uuid.uuid4()}",
@@ -330,19 +420,22 @@ def create_response(content: str, model: str, finish_reason: Optional[str] = Non
330
  "finish_reason": finish_reason,
331
  }
332
  ],
333
- "usage": None,
334
  }
335
 
336
- @app.post("/v1/chat/completions")
337
- async def chat_completions(request: ChatRequest, req: Request):
 
 
338
  logger.info(f"Received chat completions request: {request}")
339
  try:
340
  messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
341
-
 
342
  async_generator = Blackbox.create_async_generator(
343
  model=request.model,
344
  messages=messages,
345
- image=None,
346
  image_name=None,
347
  webSearchMode=request.webSearchMode
348
  )
@@ -350,15 +443,17 @@ async def chat_completions(request: ChatRequest, req: Request):
350
  if request.stream:
351
  async def generate():
352
  try:
 
353
  async for chunk in async_generator:
354
  if isinstance(chunk, ImageResponse):
355
  image_markdown = f"![image]({chunk.url})"
356
  response_chunk = create_response(image_markdown, request.model)
 
 
357
  else:
358
  response_chunk = create_response(chunk, request.model)
359
-
360
- # Yield each chunk in SSE format
361
- yield f"data: {json.dumps(response_chunk)}\n\n"
362
 
363
  # Signal the end of the stream
364
  yield "data: [DONE]\n\n"
@@ -373,34 +468,36 @@ async def chat_completions(request: ChatRequest, req: Request):
373
  return StreamingResponse(generate(), media_type="text/event-stream")
374
  else:
375
  response_content = ""
 
376
  async for chunk in async_generator:
377
  if isinstance(chunk, ImageResponse):
378
  response_content += f"![image]({chunk.url})\n"
 
379
  else:
380
  response_content += chunk
 
 
 
381
 
382
  logger.info("Completed non-streaming response generation.")
383
- return {
384
- "id": f"chatcmpl-{uuid.uuid4()}",
385
- "object": "chat.completion",
386
- "created": int(datetime.now().timestamp()),
387
- "model": request.model,
388
- "choices": [
389
- {
390
- "message": {
391
- "role": "assistant",
392
- "content": response_content
393
- },
394
- "finish_reason": "stop",
395
- "index": 0
396
- }
397
  ],
398
- "usage": {
399
- "prompt_tokens": sum(len(msg['content'].split()) for msg in messages),
400
- "completion_tokens": len(response_content.split()),
401
- "total_tokens": sum(len(msg['content'].split()) for msg in messages) + len(response_content.split())
402
- },
403
- }
404
  except ModelNotWorkingException as e:
405
  logger.warning(f"Model not working: {e}")
406
  raise HTTPException(status_code=503, detail=str(e))
@@ -411,19 +508,17 @@ async def chat_completions(request: ChatRequest, req: Request):
411
  logger.exception("An unexpected error occurred while processing the chat completions request.")
412
  raise HTTPException(status_code=500, detail=str(e))
413
 
 
414
  @app.get("/v1/models")
415
- async def get_models():
 
416
  logger.info("Fetching available models.")
417
  return {"data": [{"id": model} for model in Blackbox.models]}
418
 
419
- # Additional endpoints for better functionality
420
- @app.get("/v1/health")
421
- async def health_check():
422
- """Health check endpoint to verify the service is running."""
423
- return {"status": "ok"}
424
-
425
  @app.get("/v1/models/{model}/status")
426
- async def model_status(model: str):
 
427
  """Check if a specific model is available."""
428
  if model in Blackbox.models:
429
  return {"model": model, "status": "available"}
@@ -433,6 +528,14 @@ async def model_status(model: str):
433
  else:
434
  raise HTTPException(status_code=404, detail="Model not found")
435
 
 
 
 
 
 
 
 
 
436
  if __name__ == "__main__":
437
  import uvicorn
438
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ import os
2
  import re
3
  import random
4
  import string
 
6
  import json
7
  import logging
8
  import asyncio
9
+ import time
10
  from aiohttp import ClientSession, ClientTimeout, ClientError
11
+ from fastapi import FastAPI, HTTPException, Request, Depends, Header, status
12
+ from fastapi.responses import StreamingResponse, JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel, Field
15
  from typing import List, Dict, Any, Optional, AsyncGenerator
16
  from datetime import datetime
17
+ from slowapi import Limiter, _rate_limit_exceeded_handler
18
+ from slowapi.util import get_remote_address
19
+ from slowapi.errors import RateLimitExceeded
20
+ import tiktoken
21
+ from dotenv import load_dotenv
22
+
23
+ # Load environment variables from .env file
24
+ load_dotenv()
25
 
26
  # Configure logging
27
  logging.basicConfig(
 
33
  )
34
  logger = logging.getLogger(__name__)
35
 
36
+ # Initialize FastAPI app
37
+ app = FastAPI(title="OpenAI-Compatible API")
38
+
39
+ # Configure CORS (adjust origins as needed)
40
+ origins = [
41
+ "*", # Allow all origins; replace with specific origins in production
42
+ ]
43
+
44
+ app.add_middleware(
45
+ CORSMiddleware,
46
+ allow_origins=origins,
47
+ allow_credentials=True,
48
+ allow_methods=["*"],
49
+ allow_headers=["*"],
50
+ )
51
+
52
+ # Initialize Rate Limiter from environment variable
53
+ RATE_LIMIT = os.getenv("RATE_LIMIT", "60/minute") # Default to 60 requests per minute
54
+ limiter = Limiter(key_func=get_remote_address, default_limits=[RATE_LIMIT])
55
+ app.state.limiter = limiter
56
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
57
+
58
+ # API Key Authentication
59
+ API_KEYS = set(os.getenv("API_KEYS", "").split(",")) # Load API keys from environment variable
60
+
61
+ async def get_api_key(authorization: Optional[str] = Header(None)):
62
+ """
63
+ Dependency to validate API Key from the Authorization header.
64
+ """
65
+ if authorization is None:
66
+ raise HTTPException(
67
+ status_code=status.HTTP_401_UNAUTHORIZED,
68
+ detail="Authorization header missing",
69
+ headers={"WWW-Authenticate": "Bearer"},
70
+ )
71
+ parts = authorization.split()
72
+ if parts[0].lower() != "bearer" or len(parts) != 2:
73
+ raise HTTPException(
74
+ status_code=status.HTTP_401_UNAUTHORIZED,
75
+ detail="Invalid authorization header format",
76
+ headers={"WWW-Authenticate": "Bearer"},
77
+ )
78
+ token = parts[1]
79
+ if token not in API_KEYS:
80
+ raise HTTPException(
81
+ status_code=status.HTTP_401_UNAUTHORIZED,
82
+ detail="Invalid API Key",
83
+ headers={"WWW-Authenticate": "Bearer"},
84
+ )
85
+ return token
86
+
87
  # Custom exception for model not working
88
  class ModelNotWorkingException(Exception):
89
  def __init__(self, model: str):
 
91
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
92
  super().__init__(self.message)
93
 
94
+ # Mock implementations for ImageResponse and to_data_uri (custom functionality)
95
  class ImageResponse:
96
  def __init__(self, url: str, alt: str):
97
  self.url = url
98
  self.alt = alt
99
 
100
  def to_data_uri(image: Any) -> str:
101
+ return "data:image/png;base64,..." # Replace with actual base64 data if needed
102
+
103
+ # Token Counting using tiktoken
104
+ def count_tokens(messages: List[Dict[str, str]], model: str) -> int:
105
+ """
106
+ Counts the number of tokens in the messages using tiktoken.
107
+ Adjust the encoding based on the model.
108
+ """
109
+ try:
110
+ encoding = tiktoken.get_encoding("cl100k_base") # Adjust encoding as per model
111
+ except:
112
+ encoding = tiktoken.get_encoding("cl100k_base") # Default encoding
113
+ tokens = 0
114
+ for message in messages:
115
+ tokens += len(encoding.encode(message['content']))
116
+ return tokens
117
 
118
+ # Blackbox Class: Handles interaction with the external AI service
119
  class Blackbox:
120
  url = "https://www.blackbox.ai"
121
+ api_endpoint = os.getenv("EXTERNAL_API_ENDPOINT", "https://www.blackbox.ai/api/chat")
122
  working = True
123
  supports_stream = True
124
  supports_system_message = True
 
178
  'XcodeAgent': {'mode': True, 'id': "Xcode Agent"},
179
  'AngularJSAgent': {'mode': True, 'id': "AngularJS Agent"},
180
  }
181
+
182
  userSelectedModel = {
183
  "gpt-4o": "gpt-4o",
184
  "gemini-pro": "gemini-pro",
185
  'claude-sonnet-3.5': "claude-sonnet-3.5",
186
  }
187
+
188
  model_prefixes = {
189
  'gpt-4o': '@GPT-4o',
190
  'gemini-pro': '@Gemini-PRO',
 
206
  'ImageGeneration': '@Image Generation',
207
  'Niansuh': '@Niansuh',
208
  }
209
+
210
  model_referers = {
211
  "blackboxai": f"{url}/?model=blackboxai",
212
  "gpt-4o": f"{url}/?model=gpt-4o",
213
  "gemini-pro": f"{url}/?model=gemini-pro",
214
  "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
215
  }
216
+
217
  model_aliases = {
218
  "gemini-flash": "gemini-1.5-flash",
219
  "claude-3.5-sonnet": "claude-sonnet-3.5",
 
296
  "trendingAgentMode": {},
297
  "isMicMode": False,
298
  "userSystemPrompt": None,
299
+ "maxTokens": int(os.getenv("MAX_TOKENS", "4096")),
300
  "playgroundTopP": 0.9,
301
  "playgroundTemperature": 0.5,
302
  "isChromeExt": False,
 
372
  except ClientError as ce:
373
  logger.error(f"Client error occurred: {ce}. Retrying attempt {attempt + 1}/{retry_attempts}")
374
  if attempt == retry_attempts - 1:
375
+ raise HTTPException(status_code=502, detail="Error communicating with the external API.")
376
  except asyncio.TimeoutError:
377
  logger.error(f"Request timed out. Retrying attempt {attempt + 1}/{retry_attempts}")
378
  if attempt == retry_attempts - 1:
379
+ raise HTTPException(status_code=504, detail="External API request timed out.")
380
  except Exception as e:
381
  logger.error(f"Unexpected error: {e}. Retrying attempt {attempt + 1}/{retry_attempts}")
382
  if attempt == retry_attempts - 1:
383
  raise HTTPException(status_code=500, detail=str(e))
384
 
385
+ # Pydantic Models
 
 
386
  class Message(BaseModel):
387
+ role: str = Field(..., description="The role of the message author.")
388
+ content: str = Field(..., description="The content of the message.")
389
 
390
  class ChatRequest(BaseModel):
391
+ model: str = Field(..., description="ID of the model to use.")
392
+ messages: List[Message] = Field(..., description="A list of messages comprising the conversation.")
393
+ stream: Optional[bool] = Field(False, description="Whether to stream the response.")
394
+ webSearchMode: Optional[bool] = Field(False, description="Whether to enable web search mode.")
395
+
396
+ class ChatCompletionChoice(BaseModel):
397
+ index: int
398
+ delta: Dict[str, Any]
399
+ finish_reason: Optional[str] = None
400
+
401
+ class ChatCompletionResponse(BaseModel):
402
+ id: str
403
+ object: str
404
+ created: int
405
  model: str
406
+ choices: List[ChatCompletionChoice]
407
+ usage: Optional[Dict[str, int]] = None
 
408
 
409
+ # Utility Function to Create Response
410
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
411
  return {
412
  "id": f"chatcmpl-{uuid.uuid4()}",
 
420
  "finish_reason": finish_reason,
421
  }
422
  ],
423
+ "usage": None, # To be populated if usage metrics are available
424
  }
425
 
426
+ # Endpoint: Chat Completions
427
+ @app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
428
+ @limiter.limit("60/minute") # Example: 60 requests per minute per IP
429
+ async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
430
  logger.info(f"Received chat completions request: {request}")
431
  try:
432
  messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
433
+ prompt_tokens = count_tokens(messages, request.model)
434
+
435
  async_generator = Blackbox.create_async_generator(
436
  model=request.model,
437
  messages=messages,
438
+ image=None, # Adjust if image handling is required
439
  image_name=None,
440
  webSearchMode=request.webSearchMode
441
  )
 
443
  if request.stream:
444
  async def generate():
445
  try:
446
+ completion_tokens = 0
447
  async for chunk in async_generator:
448
  if isinstance(chunk, ImageResponse):
449
  image_markdown = f"![image]({chunk.url})"
450
  response_chunk = create_response(image_markdown, request.model)
451
+ yield f"data: {json.dumps(response_chunk)}\n\n"
452
+ completion_tokens += len(image_markdown.split())
453
  else:
454
  response_chunk = create_response(chunk, request.model)
455
+ yield f"data: {json.dumps(response_chunk)}\n\n"
456
+ completion_tokens += len(chunk.split())
 
457
 
458
  # Signal the end of the stream
459
  yield "data: [DONE]\n\n"
 
468
  return StreamingResponse(generate(), media_type="text/event-stream")
469
  else:
470
  response_content = ""
471
+ completion_tokens = 0
472
  async for chunk in async_generator:
473
  if isinstance(chunk, ImageResponse):
474
  response_content += f"![image]({chunk.url})\n"
475
+ completion_tokens += len(f"![image]({chunk.url})\n".split())
476
  else:
477
  response_content += chunk
478
+ completion_tokens += len(chunk.split())
479
+
480
+ total_tokens = prompt_tokens + completion_tokens
481
 
482
  logger.info("Completed non-streaming response generation.")
483
+ return ChatCompletionResponse(
484
+ id=f"chatcmpl-{uuid.uuid4()}",
485
+ object="chat.completion",
486
+ created=int(datetime.now().timestamp()),
487
+ model=request.model,
488
+ choices=[
489
+ ChatCompletionChoice(
490
+ index=0,
491
+ delta={"content": response_content, "role": "assistant"},
492
+ finish_reason="stop"
493
+ )
 
 
 
494
  ],
495
+ usage={
496
+ "prompt_tokens": prompt_tokens,
497
+ "completion_tokens": completion_tokens,
498
+ "total_tokens": total_tokens
499
+ }
500
+ )
501
  except ModelNotWorkingException as e:
502
  logger.warning(f"Model not working: {e}")
503
  raise HTTPException(status_code=503, detail=str(e))
 
508
  logger.exception("An unexpected error occurred while processing the chat completions request.")
509
  raise HTTPException(status_code=500, detail=str(e))
510
 
511
+ # Endpoint: List Models
512
  @app.get("/v1/models")
513
+ @limiter.limit("60/minute")
514
+ async def get_models(api_key: str = Depends(get_api_key)):
515
  logger.info("Fetching available models.")
516
  return {"data": [{"id": model} for model in Blackbox.models]}
517
 
518
+ # Endpoint: Model Status
 
 
 
 
 
519
  @app.get("/v1/models/{model}/status")
520
+ @limiter.limit("60/minute")
521
+ async def model_status(model: str, api_key: str = Depends(get_api_key)):
522
  """Check if a specific model is available."""
523
  if model in Blackbox.models:
524
  return {"model": model, "status": "available"}
 
528
  else:
529
  raise HTTPException(status_code=404, detail="Model not found")
530
 
531
+ # Endpoint: Health Check
532
+ @app.get("/v1/health")
533
+ @limiter.limit("60/minute")
534
+ async def health_check():
535
+ """Health check endpoint to verify the service is running."""
536
+ return {"status": "ok"}
537
+
538
+ # Run the application
539
  if __name__ == "__main__":
540
  import uvicorn
541
  uvicorn.run(app, host="0.0.0.0", port=8000)