Niansuh commited on
Commit
dc9182d
·
verified ·
1 Parent(s): e6f4968

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +415 -190
main.py CHANGED
@@ -1,5 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Simple in-memory rate limiter based solely on IP addresses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  async def rate_limiter_per_ip(request: Request):
 
 
 
3
  client_ip = request.client.host
4
  current_time = time.time()
5
 
@@ -9,18 +72,43 @@ async def rate_limiter_per_ip(request: Request):
9
  else:
10
  if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
11
  logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
12
- raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address')
13
  rate_limit_store[client_ip]["count"] += 1
14
 
15
- CLEANUP_INTERVAL = 60 # seconds
16
- RATE_LIMIT_WINDOW = 60 # seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  class Blackbox:
19
- label = "Blackbox AI"
20
  url = "https://www.blackbox.ai"
21
  api_endpoint = "https://www.blackbox.ai/api/chat"
22
  working = True
23
- supports_gpt_4 = True
24
  supports_stream = True
25
  supports_system_message = True
26
  supports_message_history = True
@@ -30,7 +118,6 @@ class Blackbox:
30
  models = [
31
  default_model,
32
  'blackboxai-pro',
33
- *image_models,
34
  "llama-3.1-8b",
35
  'llama-3.1-70b',
36
  'llama-3.1-405b',
@@ -51,12 +138,18 @@ class Blackbox:
51
  'ReactAgent',
52
  'XcodeAgent',
53
  'AngularJSAgent',
 
 
54
  ]
55
 
 
 
 
 
56
  agentMode = {
57
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
 
58
  }
59
-
60
  trendingAgentMode = {
61
  "blackboxai": {},
62
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
@@ -104,65 +197,33 @@ class Blackbox:
104
  'AngularJSAgent': '@AngularJS Agent',
105
  'blackboxai-pro': '@BLACKBOXAI-PRO',
106
  'ImageGeneration': '@Image Generation',
 
107
  }
108
 
109
  model_referers = {
110
- "blackboxai": "/?model=blackboxai",
111
- "gpt-4o": "/?model=gpt-4o",
112
- "gemini-pro": "/?model=gemini-pro",
113
- "claude-sonnet-3.5": "/?model=claude-sonnet-3.5"
114
  }
115
 
116
  model_aliases = {
117
  "gemini-flash": "gemini-1.5-flash",
118
  "claude-3.5-sonnet": "claude-sonnet-3.5",
119
  "flux": "ImageGeneration",
 
120
  }
121
 
122
  @classmethod
123
- def get_model(cls, model: str) -> str:
124
  if model in cls.models:
125
  return model
126
- elif model in cls.model_aliases:
 
 
127
  return cls.model_aliases[model]
128
  else:
129
- return cls.default_model
130
-
131
- @staticmethod
132
- def generate_random_string(length: int = 7) -> str:
133
- characters = string.ascii_letters + string.digits
134
- return ''.join(random.choices(characters, k=length))
135
-
136
- @staticmethod
137
- def generate_next_action() -> str:
138
- return uuid.uuid4().hex
139
-
140
- @staticmethod
141
- def generate_next_router_state_tree() -> str:
142
- router_state = [
143
- "",
144
- {
145
- "children": [
146
- "(chat)",
147
- {
148
- "children": [
149
- "__PAGE__",
150
- {}
151
- ]
152
- }
153
- ]
154
- },
155
- None,
156
- None,
157
- True
158
- ]
159
- return json.dumps(router_state)
160
-
161
- @staticmethod
162
- def clean_response(text: str) -> str:
163
- pattern = r'^\$\@\$v=undefined-rv1\$\@\$'
164
- cleaned_text = re.sub(pattern, '', text)
165
- return cleaned_text
166
 
167
  @classmethod
168
  async def create_async_generator(
@@ -170,77 +231,73 @@ class Blackbox:
170
  model: str,
171
  messages: List[Dict[str, str]],
172
  proxy: Optional[str] = None,
173
- websearch: bool = False,
 
 
174
  **kwargs
175
- ) -> AsyncGenerator[Union[str, Dict[str, Any]], None]:
176
- """
177
- Creates an asynchronous generator for streaming responses from Blackbox AI.
178
- """
179
  model = cls.get_model(model)
 
 
 
180
 
181
- chat_id = cls.generate_random_string()
182
- next_action = cls.generate_next_action()
183
- next_router_state_tree = cls.generate_next_router_state_tree()
184
-
185
- agent_mode = cls.agentMode.get(model, {})
186
- trending_agent_mode = cls.trendingAgentMode.get(model, {})
187
 
188
- prefix = cls.model_prefixes.get(model, "")
 
 
189
 
190
- formatted_prompt = ""
191
- for message in messages:
192
- role = message.get('role', '').capitalize()
193
- content = message.get('content', '')
194
- if role and content:
195
- formatted_prompt += f"{role}: {content}\n"
196
-
197
- if prefix:
198
- formatted_prompt = f"{prefix} {formatted_prompt}".strip()
199
-
200
- referer_path = cls.model_referers.get(model, f"/?model={model}")
201
- referer_url = f"{cls.url}{referer_path}"
202
-
203
- common_headers = {
204
- 'accept': '*/*',
205
- 'accept-language': 'en-US,en;q=0.9',
206
- 'cache-control': 'no-cache',
207
- 'origin': cls.url,
208
- 'pragma': 'no-cache',
209
- 'priority': 'u=1, i',
210
- 'sec-ch-ua': '"Chromium";v="129", "Not=A?Brand";v="8"',
211
- 'sec-ch-ua-mobile': '?0',
212
- 'sec-ch-ua-platform': '"Linux"',
213
- 'sec-fetch-dest': 'empty',
214
- 'sec-fetch-mode': 'cors',
215
- 'sec-fetch-site': 'same-origin',
216
- 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) '
217
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
218
- 'Chrome/129.0.0.0 Safari/537.36'
219
  }
220
 
221
- headers_api_chat = {
222
- 'Content-Type': 'application/json',
223
- 'Referer': referer_url
224
- }
225
- headers_api_chat_combined = {**common_headers, **headers_api_chat}
226
-
227
- payload_api_chat = {
228
- "messages": [
229
- {
230
- "id": chat_id,
231
- "content": formatted_prompt,
232
- "role": "user"
233
- }
234
- ],
235
- "id": chat_id,
 
 
 
 
 
 
 
 
 
 
236
  "previewToken": None,
237
  "userId": None,
238
  "codeModelMode": True,
239
- "agentMode": agent_mode,
240
- "trendingAgentMode": trending_agent_mode,
241
  "isMicMode": False,
242
  "userSystemPrompt": None,
243
- "maxTokens": 1024,
244
  "playgroundTopP": 0.9,
245
  "playgroundTemperature": 0.5,
246
  "isChromeExt": False,
@@ -250,66 +307,81 @@ class Blackbox:
250
  "clickedForceWebSearch": False,
251
  "visitFromDelta": False,
252
  "mobileClient": False,
253
- "webSearchMode": websearch,
254
- "userSelectedModel": cls.userSelectedModel.get(model, model)
255
  }
256
 
257
- async with ClientSession(headers=common_headers) as session:
 
 
 
 
 
 
 
 
 
 
 
258
  try:
259
- async with session.post(
260
- cls.api_endpoint,
261
- headers=headers_api_chat_combined,
262
- json=payload_api_chat,
263
- proxy=proxy
264
- ) as response_api_chat:
265
- response_api_chat.raise_for_status()
266
- text = await response_api_chat.text()
267
- cleaned_response = cls.clean_response(text)
268
-
269
- if model in cls.image_models:
270
- match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_response)
271
- if match:
272
- image_url = match.group(1)
273
- yield {"type": "image", "url": image_url, "alt": "Generated Image"}
274
- else:
275
- yield cleaned_response
276
- else:
277
- if websearch:
278
- match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_response, re.DOTALL)
279
- if match:
280
- source_part = match.group(1).strip()
281
- answer_part = cleaned_response[match.end():].strip()
282
- try:
283
- sources = json.loads(source_part)
284
- source_formatted = "**Source:**\n"
285
- for item in sources:
286
- title = item.get('title', 'No Title')
287
- link = item.get('link', '#')
288
- position = item.get('position', '')
289
- source_formatted += f"{position}. [{title}]({link})\n"
290
- final_response = f"{answer_part}\n\n{source_formatted}"
291
- except json.JSONDecodeError:
292
- final_response = f"{answer_part}\n\nSource information is unavailable."
293
  else:
294
- final_response = cleaned_response
 
295
  else:
296
- if '$~~~$' in cleaned_response:
297
- final_response = cleaned_response.split('$~~~$')[0].strip()
298
- else:
299
- final_response = cleaned_response
300
-
301
- yield final_response
302
- except ClientResponseError as e:
303
- error_text = f"Error {e.status}: {e.message}"
304
- try:
305
- error_response = await e.response.text()
306
- cleaned_error = cls.clean_response(error_response)
307
- error_text += f" - {cleaned_error}"
308
- except Exception:
309
- pass
310
- yield error_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  except Exception as e:
312
- yield f"Unexpected error during /api/chat request: {str(e)}"
 
 
313
 
314
  # FastAPI app setup
315
  app = FastAPI()
@@ -354,13 +426,47 @@ class ChatRequest(BaseModel):
354
  temperature: Optional[float] = 1.0
355
  top_p: Optional[float] = 1.0
356
  n: Optional[int] = 1
 
 
357
  max_tokens: Optional[int] = None
358
  presence_penalty: Optional[float] = 0.0
359
  frequency_penalty: Optional[float] = 0.0
360
  logit_bias: Optional[Dict[str, float]] = None
361
  user: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
- @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip: Request)])
364
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
365
  client_ip = req.client.host
366
  # Redact user messages only for logging purposes
@@ -375,35 +481,119 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
375
  raise HTTPException(status_code=400, detail="Requested model is not available.")
376
 
377
  # Process the request with actual message content, but don't log it
378
- response_content = await Blackbox.create_async_generator(
379
  model=request.model,
380
- messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
381
- temperature=request.temperature,
382
- max_tokens=request.max_tokens
 
383
  )
384
 
385
- logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
386
- return {
387
- "id": f"chatcmpl-{uuid.uuid4()}",
388
- "object": "chat.completion",
389
- "created": int(datetime.now().timestamp()),
390
- "model": request.model,
391
- "choices": [
392
- {
393
- "index": 0,
394
- "message": {
395
- "role": "assistant",
396
- "content": response_content
397
- },
398
- "finish_reason": "stop"
399
- }
400
- ],
401
- "usage": {
402
- "prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
403
- "completion_tokens": len(response_content.split()),
404
- "total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.split())
405
- },
406
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  except ModelNotWorkingException as e:
408
  logger.warning(f"Model not working: {e} | IP: {client_ip}")
409
  raise HTTPException(status_code=503, detail=str(e))
@@ -414,6 +604,15 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
414
  logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
415
  raise HTTPException(status_code=500, detail=str(e))
416
 
 
 
 
 
 
 
 
 
 
417
  # Endpoint: GET /v1/models
418
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
419
  async def get_models(req: Request):
@@ -421,6 +620,20 @@ async def get_models(req: Request):
421
  logger.info(f"Fetching available models from IP: {client_ip}")
422
  return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  # Endpoint: GET /v1/health
425
  @app.get("/v1/health", dependencies=[Depends(rate_limiter_per_ip)])
426
  async def health_check(req: Request):
@@ -428,6 +641,13 @@ async def health_check(req: Request):
428
  logger.info(f"Health check requested from IP: {client_ip}")
429
  return {"status": "ok"}
430
 
 
 
 
 
 
 
 
431
  # Custom exception handler to match OpenAI's error format
432
  @app.exception_handler(HTTPException)
433
  async def http_exception_handler(request: Request, exc: HTTPException):
@@ -444,3 +664,8 @@ async def http_exception_handler(request: Request, exc: HTTPException):
444
  }
445
  },
446
  )
 
 
 
 
 
 
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
+
13
+ from datetime import datetime
14
+
15
+ from aiohttp import ClientSession, ClientTimeout, ClientError
16
+ from fastapi import FastAPI, HTTPException, Request, Depends, Header
17
+ from fastapi.responses import StreamingResponse, JSONResponse, RedirectResponse
18
+ from pydantic import BaseModel
19
+
20
+ # Configure logging
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
24
+ handlers=[logging.StreamHandler()]
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Load environment variables
29
+ API_KEYS = os.getenv('API_KEYS', '').split(',') # Comma-separated API keys
30
+ RATE_LIMIT = int(os.getenv('RATE_LIMIT', '60')) # Requests per minute
31
+ AVAILABLE_MODELS = os.getenv('AVAILABLE_MODELS', '') # Comma-separated available models
32
+
33
+ if not API_KEYS or API_KEYS == ['']:
34
+ logger.error("No API keys found. Please set the API_KEYS environment variable.")
35
+ raise Exception("API_KEYS environment variable not set.")
36
+
37
+ # Process available models
38
+ if AVAILABLE_MODELS:
39
+ AVAILABLE_MODELS = [model.strip() for model in AVAILABLE_MODELS.split(',') if model.strip()]
40
+ else:
41
+ AVAILABLE_MODELS = [] # If empty, all models are available
42
+
43
  # Simple in-memory rate limiter based solely on IP addresses
44
+ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
45
+
46
+ # Define cleanup interval and window
47
+ CLEANUP_INTERVAL = 60 # seconds
48
+ RATE_LIMIT_WINDOW = 60 # seconds
49
+
50
+ async def cleanup_rate_limit_stores():
51
+ """
52
+ Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
53
+ """
54
+ while True:
55
+ current_time = time.time()
56
+ ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
57
+ for ip in ips_to_delete:
58
+ del rate_limit_store[ip]
59
+ logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
60
+ await asyncio.sleep(CLEANUP_INTERVAL)
61
+
62
  async def rate_limiter_per_ip(request: Request):
63
+ """
64
+ Rate limiter that enforces a limit based on the client's IP address.
65
+ """
66
  client_ip = request.client.host
67
  current_time = time.time()
68
 
 
72
  else:
73
  if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
74
  logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
75
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
76
  rate_limit_store[client_ip]["count"] += 1
77
 
78
+ async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
79
+ """
80
+ Dependency to extract and validate the API key from the Authorization header.
81
+ """
82
+ client_ip = request.client.host
83
+ if authorization is None or not authorization.startswith('Bearer '):
84
+ logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
85
+ raise HTTPException(status_code=401, detail='Invalid authorization header format')
86
+ api_key = authorization[7:]
87
+ if api_key not in API_KEYS:
88
+ logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
89
+ raise HTTPException(status_code=401, detail='Invalid API key')
90
+ return api_key
91
+
92
+ # Custom exception for model not working
93
+ class ModelNotWorkingException(Exception):
94
+ def __init__(self, model: str):
95
+ self.model = model
96
+ self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
97
+ super().__init__(self.message)
98
+
99
+ # Mock implementations for ImageResponse and to_data_uri
100
+ class ImageResponse:
101
+ def __init__(self, url: str, alt: str):
102
+ self.url = url
103
+ self.alt = alt
104
+
105
+ def to_data_uri(image: Any) -> str:
106
+ return "data:image/png;base64,..." # Replace with actual base64 data
107
 
108
  class Blackbox:
 
109
  url = "https://www.blackbox.ai"
110
  api_endpoint = "https://www.blackbox.ai/api/chat"
111
  working = True
 
112
  supports_stream = True
113
  supports_system_message = True
114
  supports_message_history = True
 
118
  models = [
119
  default_model,
120
  'blackboxai-pro',
 
121
  "llama-3.1-8b",
122
  'llama-3.1-70b',
123
  'llama-3.1-405b',
 
138
  'ReactAgent',
139
  'XcodeAgent',
140
  'AngularJSAgent',
141
+ *image_models,
142
+ 'Niansuh',
143
  ]
144
 
145
+ # Filter models based on AVAILABLE_MODELS
146
+ if AVAILABLE_MODELS:
147
+ models = [model for model in models if model in AVAILABLE_MODELS]
148
+
149
  agentMode = {
150
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
151
+ 'Niansuh': {'mode': True, 'id': "NiansuhAIk1HgESy", 'name': "Niansuh"},
152
  }
 
153
  trendingAgentMode = {
154
  "blackboxai": {},
155
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
 
197
  'AngularJSAgent': '@AngularJS Agent',
198
  'blackboxai-pro': '@BLACKBOXAI-PRO',
199
  'ImageGeneration': '@Image Generation',
200
+ 'Niansuh': '@Niansuh',
201
  }
202
 
203
  model_referers = {
204
+ "blackboxai": f"{url}/?model=blackboxai",
205
+ "gpt-4o": f"{url}/?model=gpt-4o",
206
+ "gemini-pro": f"{url}/?model=gemini-pro",
207
+ "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
208
  }
209
 
210
  model_aliases = {
211
  "gemini-flash": "gemini-1.5-flash",
212
  "claude-3.5-sonnet": "claude-sonnet-3.5",
213
  "flux": "ImageGeneration",
214
+ "niansuh": "Niansuh",
215
  }
216
 
217
  @classmethod
218
+ def get_model(cls, model: str) -> Optional[str]:
219
  if model in cls.models:
220
  return model
221
+ elif model in cls.userSelectedModel and cls.userSelectedModel[model] in cls.models:
222
+ return cls.userSelectedModel[model]
223
+ elif model in cls.model_aliases and cls.model_aliases[model] in cls.models:
224
  return cls.model_aliases[model]
225
  else:
226
+ return cls.default_model if cls.default_model in cls.models else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  @classmethod
229
  async def create_async_generator(
 
231
  model: str,
232
  messages: List[Dict[str, str]],
233
  proxy: Optional[str] = None,
234
+ image: Any = None,
235
+ image_name: Optional[str] = None,
236
+ webSearchMode: bool = False,
237
  **kwargs
238
+ ) -> AsyncGenerator[Any, None]:
 
 
 
239
  model = cls.get_model(model)
240
+ if model is None:
241
+ logger.error(f"Model {model} is not available.")
242
+ raise ModelNotWorkingException(model)
243
 
244
+ logger.info(f"Selected model: {model}")
 
 
 
 
 
245
 
246
+ if not cls.working or model not in cls.models:
247
+ logger.error(f"Model {model} is not working or not supported.")
248
+ raise ModelNotWorkingException(model)
249
 
250
+ headers = {
251
+ "accept": "*/*",
252
+ "accept-language": "en-US,en;q=0.9",
253
+ "cache-control": "no-cache",
254
+ "content-type": "application/json",
255
+ "origin": cls.url,
256
+ "pragma": "no-cache",
257
+ "priority": "u=1, i",
258
+ "referer": cls.model_referers.get(model, cls.url),
259
+ "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"',
260
+ "sec-ch-ua-mobile": "?0",
261
+ "sec-ch-ua-platform": '"Linux"',
262
+ "sec-fetch-dest": "empty",
263
+ "sec-fetch-mode": "cors",
264
+ "sec-fetch-site": "same-origin",
265
+ "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
+ if model in cls.model_prefixes:
269
+ prefix = cls.model_prefixes[model]
270
+ if not messages[0]['content'].startswith(prefix):
271
+ logger.debug(f"Adding prefix '{prefix}' to the first message.")
272
+ messages[0]['content'] = f"{prefix} {messages[0]['content']}"
273
+
274
+ random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
275
+ messages[-1]['id'] = random_id
276
+ messages[-1]['role'] = 'user'
277
+
278
+ # Don't log the full message content for privacy
279
+ logger.debug(f"Generated message ID: {random_id} for model: {model}")
280
+
281
+ if image is not None:
282
+ messages[-1]['data'] = {
283
+ 'fileText': '',
284
+ 'imageBase64': to_data_uri(image),
285
+ 'title': image_name
286
+ }
287
+ messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
288
+ logger.debug("Image data added to the message.")
289
+
290
+ data = {
291
+ "messages": messages,
292
+ "id": random_id,
293
  "previewToken": None,
294
  "userId": None,
295
  "codeModelMode": True,
296
+ "agentMode": {},
297
+ "trendingAgentMode": {},
298
  "isMicMode": False,
299
  "userSystemPrompt": None,
300
+ "maxTokens": 99999999,
301
  "playgroundTopP": 0.9,
302
  "playgroundTemperature": 0.5,
303
  "isChromeExt": False,
 
307
  "clickedForceWebSearch": False,
308
  "visitFromDelta": False,
309
  "mobileClient": False,
310
+ "userSelectedModel": None,
311
+ "webSearchMode": webSearchMode,
312
  }
313
 
314
+ if model in cls.agentMode:
315
+ data["agentMode"] = cls.agentMode[model]
316
+ elif model in cls.trendingAgentMode:
317
+ data["trendingAgentMode"] = cls.trendingAgentMode[model]
318
+ elif model in cls.userSelectedModel:
319
+ data["userSelectedModel"] = cls.userSelectedModel[model]
320
+ logger.info(f"Sending request to {cls.api_endpoint} with data (excluding messages).")
321
+
322
+ timeout = ClientTimeout(total=60) # Set an appropriate timeout
323
+ retry_attempts = 10 # Set the number of retry attempts
324
+
325
+ for attempt in range(retry_attempts):
326
  try:
327
+ async with ClientSession(headers=headers, timeout=timeout) as session:
328
+ async with session.post(cls.api_endpoint, json=data, proxy=proxy) as response:
329
+ response.raise_for_status()
330
+ logger.info(f"Received response with status {response.status}")
331
+ if model == 'ImageGeneration':
332
+ response_text = await response.text()
333
+ url_match = re.search(r'https://storage\.googleapis\.com/[^\s\)]+', response_text)
334
+ if url_match:
335
+ image_url = url_match.group(0)
336
+ logger.info(f"Image URL found.")
337
+ yield ImageResponse(image_url, alt=messages[-1]['content'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  else:
339
+ logger.error("Image URL not found in the response.")
340
+ raise Exception("Image URL not found in the response")
341
  else:
342
+ full_response = ""
343
+ search_results_json = ""
344
+ try:
345
+ async for chunk, _ in response.content.iter_chunks():
346
+ if chunk:
347
+ decoded_chunk = chunk.decode(errors='ignore')
348
+ decoded_chunk = re.sub(r'\$@\$v=[^$]+\$@\$', '', decoded_chunk)
349
+ if decoded_chunk.strip():
350
+ if '$~~~$' in decoded_chunk:
351
+ search_results_json += decoded_chunk
352
+ else:
353
+ full_response += decoded_chunk
354
+ yield decoded_chunk
355
+ logger.info("Finished streaming response chunks.")
356
+ except Exception as e:
357
+ logger.exception("Error while iterating over response chunks.")
358
+ raise e
359
+ if data["webSearchMode"] and search_results_json:
360
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', search_results_json, re.DOTALL)
361
+ if match:
362
+ try:
363
+ search_results = json.loads(match.group(1))
364
+ formatted_results = "\n\n**Sources:**\n"
365
+ for i, result in enumerate(search_results[:5], 1):
366
+ formatted_results += f"{i}. [{result['title']}]({result['link']})\n"
367
+ logger.info("Formatted search results.")
368
+ yield formatted_results
369
+ except json.JSONDecodeError as je:
370
+ logger.error("Failed to parse search results JSON.")
371
+ raise je
372
+ break # Exit the retry loop if successful
373
+ except ClientError as ce:
374
+ logger.error(f"Client error occurred: {ce}. Retrying attempt {attempt + 1}/{retry_attempts}")
375
+ if attempt == retry_attempts - 1:
376
+ raise HTTPException(status_code=502, detail="Error communicating with the external API.")
377
+ except asyncio.TimeoutError:
378
+ logger.error(f"Request timed out. Retrying attempt {attempt + 1}/{retry_attempts}")
379
+ if attempt == retry_attempts - 1:
380
+ raise HTTPException(status_code=504, detail="External API request timed out.")
381
  except Exception as e:
382
+ logger.error(f"Unexpected error: {e}. Retrying attempt {attempt + 1}/{retry_attempts}")
383
+ if attempt == retry_attempts - 1:
384
+ raise HTTPException(status_code=500, detail=str(e))
385
 
386
  # FastAPI app setup
387
  app = FastAPI()
 
426
  temperature: Optional[float] = 1.0
427
  top_p: Optional[float] = 1.0
428
  n: Optional[int] = 1
429
+ stream: Optional[bool] = False
430
+ stop: Optional[Union[str, List[str]]] = None
431
  max_tokens: Optional[int] = None
432
  presence_penalty: Optional[float] = 0.0
433
  frequency_penalty: Optional[float] = 0.0
434
  logit_bias: Optional[Dict[str, float]] = None
435
  user: Optional[str] = None
436
+ webSearchMode: Optional[bool] = False # Custom parameter
437
+
438
+ class TokenizerRequest(BaseModel):
439
+ text: str
440
+
441
+ def calculate_estimated_cost(prompt_tokens: int, completion_tokens: int) -> float:
442
+ """
443
+ Calculate the estimated cost based on the number of tokens.
444
+ Replace the pricing below with your actual pricing model.
445
+ """
446
+ # Example pricing: $0.00000268 per token
447
+ cost_per_token = 0.00000268
448
+ return round((prompt_tokens + completion_tokens) * cost_per_token, 8)
449
+
450
+ def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
451
+ return {
452
+ "id": f"chatcmpl-{uuid.uuid4()}",
453
+ "object": "chat.completion",
454
+ "created": int(datetime.now().timestamp()),
455
+ "model": model,
456
+ "choices": [
457
+ {
458
+ "index": 0,
459
+ "message": {
460
+ "role": "assistant",
461
+ "content": content
462
+ },
463
+ "finish_reason": finish_reason
464
+ }
465
+ ],
466
+ "usage": None, # To be filled in non-streaming responses
467
+ }
468
 
469
+ @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
470
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
471
  client_ip = req.client.host
472
  # Redact user messages only for logging purposes
 
481
  raise HTTPException(status_code=400, detail="Requested model is not available.")
482
 
483
  # Process the request with actual message content, but don't log it
484
+ async_generator = Blackbox.create_async_generator(
485
  model=request.model,
486
+ messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
487
+ image=None,
488
+ image_name=None,
489
+ webSearchMode=request.webSearchMode
490
  )
491
 
492
+ if request.stream:
493
+ async def generate():
494
+ try:
495
+ assistant_content = ""
496
+ async for chunk in async_generator:
497
+ if isinstance(chunk, ImageResponse):
498
+ # Handle image responses if necessary
499
+ image_markdown = f"![image]({chunk.url})\n"
500
+ assistant_content += image_markdown
501
+ response_chunk = create_response(image_markdown, request.model, finish_reason=None)
502
+ else:
503
+ assistant_content += chunk
504
+ # Yield the chunk as a partial choice
505
+ response_chunk = {
506
+ "id": f"chatcmpl-{uuid.uuid4()}",
507
+ "object": "chat.completion.chunk",
508
+ "created": int(datetime.now().timestamp()),
509
+ "model": request.model,
510
+ "choices": [
511
+ {
512
+ "index": 0,
513
+ "delta": {"content": chunk, "role": "assistant"},
514
+ "finish_reason": None,
515
+ }
516
+ ],
517
+ "usage": None, # Usage can be updated if you track tokens in real-time
518
+ }
519
+ yield f"data: {json.dumps(response_chunk)}\n\n"
520
+
521
+ # After all chunks are sent, send the final message with finish_reason
522
+ prompt_tokens = sum(len(msg['content'].split()) for msg in request.messages)
523
+ completion_tokens = len(assistant_content.split())
524
+ total_tokens = prompt_tokens + completion_tokens
525
+ estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
526
+
527
+ final_response = {
528
+ "id": f"chatcmpl-{uuid.uuid4()}",
529
+ "object": "chat.completion",
530
+ "created": int(datetime.now().timestamp()),
531
+ "model": request.model,
532
+ "choices": [
533
+ {
534
+ "message": {
535
+ "role": "assistant",
536
+ "content": assistant_content
537
+ },
538
+ "finish_reason": "stop",
539
+ "index": 0
540
+ }
541
+ ],
542
+ "usage": {
543
+ "prompt_tokens": prompt_tokens,
544
+ "completion_tokens": completion_tokens,
545
+ "total_tokens": total_tokens,
546
+ "estimated_cost": estimated_cost
547
+ },
548
+ }
549
+ yield f"data: {json.dumps(final_response)}\n\n"
550
+ yield "data: [DONE]\n\n"
551
+ except HTTPException as he:
552
+ error_response = {"error": he.detail}
553
+ yield f"data: {json.dumps(error_response)}\n\n"
554
+ except Exception as e:
555
+ logger.exception(f"Error during streaming response generation from IP: {client_ip}.")
556
+ error_response = {"error": str(e)}
557
+ yield f"data: {json.dumps(error_response)}\n\n"
558
+
559
+ return StreamingResponse(generate(), media_type="text/event-stream")
560
+ else:
561
+ response_content = ""
562
+ async for chunk in async_generator:
563
+ if isinstance(chunk, ImageResponse):
564
+ response_content += f"![image]({chunk.url})\n"
565
+ else:
566
+ response_content += chunk
567
+
568
+ prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
569
+ completion_tokens = len(response_content.split())
570
+ total_tokens = prompt_tokens + completion_tokens
571
+ estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
572
+
573
+ logger.info(f"Completed non-streaming response generation for API key: {api_key} | IP: {client_ip}")
574
+
575
+ return {
576
+ "id": f"chatcmpl-{uuid.uuid4()}",
577
+ "object": "chat.completion",
578
+ "created": int(datetime.now().timestamp()),
579
+ "model": request.model,
580
+ "choices": [
581
+ {
582
+ "message": {
583
+ "role": "assistant",
584
+ "content": response_content
585
+ },
586
+ "finish_reason": "stop",
587
+ "index": 0
588
+ }
589
+ ],
590
+ "usage": {
591
+ "prompt_tokens": prompt_tokens,
592
+ "completion_tokens": completion_tokens,
593
+ "total_tokens": total_tokens,
594
+ "estimated_cost": estimated_cost
595
+ },
596
+ }
597
  except ModelNotWorkingException as e:
598
  logger.warning(f"Model not working: {e} | IP: {client_ip}")
599
  raise HTTPException(status_code=503, detail=str(e))
 
604
  logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
605
  raise HTTPException(status_code=500, detail=str(e))
606
 
607
+ # Endpoint: POST /v1/tokenizer
608
+ @app.post("/v1/tokenizer", dependencies=[Depends(rate_limiter_per_ip)])
609
+ async def tokenizer(request: TokenizerRequest, req: Request):
610
+ client_ip = req.client.host
611
+ text = request.text
612
+ token_count = len(text.split())
613
+ logger.info(f"Tokenizer requested from IP: {client_ip} | Text length: {len(text)}")
614
+ return {"text": text, "tokens": token_count}
615
+
616
  # Endpoint: GET /v1/models
617
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
618
  async def get_models(req: Request):
 
620
  logger.info(f"Fetching available models from IP: {client_ip}")
621
  return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
622
 
623
+ # Endpoint: GET /v1/models/{model}/status
624
+ @app.get("/v1/models/{model}/status", dependencies=[Depends(rate_limiter_per_ip)])
625
+ async def model_status(model: str, req: Request):
626
+ client_ip = req.client.host
627
+ logger.info(f"Model status requested for '{model}' from IP: {client_ip}")
628
+ if model in Blackbox.models:
629
+ return {"model": model, "status": "available"}
630
+ elif model in Blackbox.model_aliases and Blackbox.model_aliases[model] in Blackbox.models:
631
+ actual_model = Blackbox.model_aliases[model]
632
+ return {"model": actual_model, "status": "available via alias"}
633
+ else:
634
+ logger.warning(f"Model not found: {model} from IP: {client_ip}")
635
+ raise HTTPException(status_code=404, detail="Model not found")
636
+
637
  # Endpoint: GET /v1/health
638
  @app.get("/v1/health", dependencies=[Depends(rate_limiter_per_ip)])
639
  async def health_check(req: Request):
 
641
  logger.info(f"Health check requested from IP: {client_ip}")
642
  return {"status": "ok"}
643
 
644
+ # Endpoint: GET /v1/chat/completions (GET method)
645
+ @app.get("/v1/chat/completions")
646
+ async def chat_completions_get(req: Request):
647
+ client_ip = req.client.host
648
+ logger.info(f"GET request made to /v1/chat/completions from IP: {client_ip}, redirecting to 'about:blank'")
649
+ return RedirectResponse(url='about:blank')
650
+
651
  # Custom exception handler to match OpenAI's error format
652
  @app.exception_handler(HTTPException)
653
  async def http_exception_handler(request: Request, exc: HTTPException):
 
664
  }
665
  },
666
  )
667
+
668
+ # Run the application
669
+ if __name__ == "__main__":
670
+ import uvicorn
671
+ uvicorn.run(app, host="0.0.0.0", port=8000)