Niansuh commited on
Commit
c30da33
·
verified ·
1 Parent(s): b6f5e93

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +258 -201
main.py CHANGED
@@ -11,7 +11,7 @@ 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, RedirectResponse
17
  from pydantic import BaseModel
@@ -46,68 +46,17 @@ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
46
  CLEANUP_INTERVAL = 60 # seconds
47
  RATE_LIMIT_WINDOW = 60 # seconds
48
 
49
- async def cleanup_rate_limit_stores():
50
- """
51
- Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
52
- """
53
- while True:
54
- current_time = time.time()
55
- ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
56
- for ip in ips_to_delete:
57
- del rate_limit_store[ip]
58
- logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
59
- await asyncio.sleep(CLEANUP_INTERVAL)
60
-
61
- async def rate_limiter_per_ip(request: Request):
62
- """
63
- Rate limiter that enforces a limit based on the client's IP address.
64
- """
65
- client_ip = request.client.host
66
- current_time = time.time()
67
-
68
- # Initialize or update the count and timestamp
69
- if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
70
- rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
71
- else:
72
- if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
73
- logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
74
- raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
75
- rate_limit_store[client_ip]["count"] += 1
76
-
77
- async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
78
- """
79
- Dependency to extract and validate the API key from the Authorization header.
80
- """
81
- client_ip = request.client.host
82
- if authorization is None or not authorization.startswith('Bearer '):
83
- logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
84
- raise HTTPException(status_code=401, detail='Invalid authorization header format')
85
- api_key = authorization[7:]
86
- if api_key not in API_KEYS:
87
- logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
88
- raise HTTPException(status_code=401, detail='Invalid API key')
89
- return api_key
90
-
91
- # Custom exception for model not working
92
- class ModelNotWorkingException(Exception):
93
- def __init__(self, model: str):
94
- self.model = model
95
- self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
96
- super().__init__(self.message)
97
-
98
- # Mock implementations for ImageResponse and to_data_uri
99
  class ImageResponse:
100
- def __init__(self, url: str, alt: str):
101
- self.url = url
102
  self.alt = alt
103
 
104
- def to_data_uri(image: Any) -> str:
105
- return "data:image/png;base64,..." # Replace with actual base64 data
106
-
107
  class Blackbox:
 
108
  url = "https://www.blackbox.ai"
109
  api_endpoint = "https://www.blackbox.ai/api/chat"
110
  working = True
 
111
  supports_stream = True
112
  supports_system_message = True
113
  supports_message_history = True
@@ -117,6 +66,7 @@ class Blackbox:
117
  models = [
118
  default_model,
119
  'blackboxai-pro',
 
120
  "llama-3.1-8b",
121
  'llama-3.1-70b',
122
  'llama-3.1-405b',
@@ -137,18 +87,12 @@ class Blackbox:
137
  'ReactAgent',
138
  'XcodeAgent',
139
  'AngularJSAgent',
140
- *image_models,
141
- 'Niansuh',
142
  ]
143
 
144
- # Filter models based on AVAILABLE_MODELS
145
- if AVAILABLE_MODELS:
146
- models = [model for model in models if model in AVAILABLE_MODELS]
147
-
148
  agentMode = {
149
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
150
- 'Niansuh': {'mode': True, 'id': "NiansuhAIk1HgESy", 'name': "Niansuh"},
151
  }
 
152
  trendingAgentMode = {
153
  "blackboxai": {},
154
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
@@ -196,33 +140,65 @@ class Blackbox:
196
  'AngularJSAgent': '@AngularJS Agent',
197
  'blackboxai-pro': '@BLACKBOXAI-PRO',
198
  'ImageGeneration': '@Image Generation',
199
- 'Niansuh': '@Niansuh',
200
  }
201
 
202
  model_referers = {
203
- "blackboxai": f"{url}/?model=blackboxai",
204
- "gpt-4o": f"{url}/?model=gpt-4o",
205
- "gemini-pro": f"{url}/?model=gemini-pro",
206
- "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
207
  }
208
 
209
  model_aliases = {
210
  "gemini-flash": "gemini-1.5-flash",
211
  "claude-3.5-sonnet": "claude-sonnet-3.5",
212
  "flux": "ImageGeneration",
213
- "niansuh": "Niansuh",
214
  }
215
 
216
  @classmethod
217
- def get_model(cls, model: str) -> Optional[str]:
218
  if model in cls.models:
219
  return model
220
- elif model in cls.userSelectedModel and cls.userSelectedModel[model] in cls.models:
221
- return model
222
- elif model in cls.model_aliases and cls.model_aliases[model] in cls.models:
223
  return cls.model_aliases[model]
224
  else:
225
- return cls.default_model if cls.default_model in cls.models else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
  @classmethod
228
  async def create_async_generator(
@@ -230,73 +206,88 @@ class Blackbox:
230
  model: str,
231
  messages: List[Dict[str, str]],
232
  proxy: Optional[str] = None,
233
- image: Any = None,
234
- image_name: Optional[str] = None,
235
- webSearchMode: bool = False,
236
  **kwargs
237
- ) -> AsyncGenerator[Any, None]:
 
 
 
 
 
 
 
 
 
 
 
238
  model = cls.get_model(model)
239
  if model is None:
240
  logger.error(f"Model {model} is not available.")
241
  raise ModelNotWorkingException(model)
242
 
243
- logger.info(f"Selected model: {model}")
 
 
244
 
245
- if not cls.working or model not in cls.models:
246
- logger.error(f"Model {model} is not working or not supported.")
247
- raise ModelNotWorkingException(model)
248
-
249
- headers = {
250
- "accept": "*/*",
251
- "accept-language": "en-US,en;q=0.9",
252
- "cache-control": "no-cache",
253
- "content-type": "application/json",
254
- "origin": cls.url,
255
- "pragma": "no-cache",
256
- "priority": "u=1, i",
257
- "referer": cls.model_referers.get(model, cls.url),
258
- "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"',
259
- "sec-ch-ua-mobile": "?0",
260
- "sec-ch-ua-platform": '"Linux"',
261
- "sec-fetch-dest": "empty",
262
- "sec-fetch-mode": "cors",
263
- "sec-fetch-site": "same-origin",
264
- "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
265
- }
266
 
267
- if model in cls.model_prefixes:
268
- prefix = cls.model_prefixes[model]
269
- if not messages[0]['content'].startswith(prefix):
270
- logger.debug(f"Adding prefix '{prefix}' to the first message.")
271
- messages[0]['content'] = f"{prefix} {messages[0]['content']}"
272
 
273
- random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
274
- messages[-1]['id'] = random_id
275
- messages[-1]['role'] = 'user'
276
-
277
- # Don't log the full message content for privacy
278
- logger.debug(f"Generated message ID: {random_id} for model: {model}")
279
-
280
- if image is not None:
281
- messages[-1]['data'] = {
282
- 'fileText': '',
283
- 'imageBase64': to_data_uri(image),
284
- 'title': image_name
285
- }
286
- messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
287
- logger.debug("Image data added to the message.")
288
 
289
- data = {
290
- "messages": messages,
291
- "id": random_id,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  "previewToken": None,
293
  "userId": None,
294
  "codeModelMode": True,
295
- "agentMode": {},
296
- "trendingAgentMode": {},
297
  "isMicMode": False,
298
  "userSystemPrompt": None,
299
- "maxTokens": 99999999,
300
  "playgroundTopP": 0.9,
301
  "playgroundTemperature": 0.5,
302
  "isChromeExt": False,
@@ -306,81 +297,151 @@ class Blackbox:
306
  "clickedForceWebSearch": False,
307
  "visitFromDelta": False,
308
  "mobileClient": False,
309
- "userSelectedModel": None,
310
- "webSearchMode": webSearchMode,
311
  }
312
 
313
- if model in cls.agentMode:
314
- data["agentMode"] = cls.agentMode[model]
315
- elif model in cls.trendingAgentMode:
316
- data["trendingAgentMode"] = cls.trendingAgentMode[model]
317
- elif model in cls.userSelectedModel:
318
- data["userSelectedModel"] = cls.userSelectedModel[model]
319
- logger.info(f"Sending request to {cls.api_endpoint} with data (excluding messages).")
 
 
320
 
321
- timeout = ClientTimeout(total=60) # Set an appropriate timeout
322
- retry_attempts = 10 # Set the number of retry attempts
323
 
324
- for attempt in range(retry_attempts):
325
  try:
326
- async with ClientSession(headers=headers, timeout=timeout) as session:
327
- async with session.post(cls.api_endpoint, json=data, proxy=proxy) as response:
328
- response.raise_for_status()
329
- logger.info(f"Received response with status {response.status}")
330
- if model == 'ImageGeneration':
331
- response_text = await response.text()
332
- url_match = re.search(r'https://storage\.googleapis\.com/[^\s\)]+', response_text)
333
- if url_match:
334
- image_url = url_match.group(0)
335
- logger.info(f"Image URL found.")
336
- yield ImageResponse(image_url, alt=messages[-1]['content'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  else:
338
- logger.error("Image URL not found in the response.")
339
- raise Exception("Image URL not found in the response")
340
  else:
341
- full_response = ""
342
- search_results_json = ""
343
- try:
344
- async for chunk, _ in response.content.iter_chunks():
345
- if chunk:
346
- decoded_chunk = chunk.decode(errors='ignore')
347
- decoded_chunk = re.sub(r'\$@\$v=[^$]+\$@\$', '', decoded_chunk)
348
- if decoded_chunk.strip():
349
- if '$~~~$' in decoded_chunk:
350
- search_results_json += decoded_chunk
351
- else:
352
- full_response += decoded_chunk
353
- yield decoded_chunk
354
- logger.info("Finished streaming response chunks.")
355
- except Exception as e:
356
- logger.exception("Error while iterating over response chunks.")
357
- raise e
358
- if data["webSearchMode"] and search_results_json:
359
- match = re.search(r'\$~~~\$(.*?)\$~~~\$', search_results_json, re.DOTALL)
360
- if match:
361
- try:
362
- search_results = json.loads(match.group(1))
363
- formatted_results = "\n\n**Sources:**\n"
364
- for i, result in enumerate(search_results[:5], 1):
365
- formatted_results += f"{i}. [{result['title']}]({result['link']})\n"
366
- logger.info("Formatted search results.")
367
- yield formatted_results
368
- except json.JSONDecodeError as je:
369
- logger.error("Failed to parse search results JSON.")
370
- raise je
371
- break # Exit the retry loop if successful
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
  # FastAPI app setup
386
  app = FastAPI()
@@ -432,7 +493,7 @@ class ChatRequest(BaseModel):
432
  frequency_penalty: Optional[float] = 0.0
433
  logit_bias: Optional[Dict[str, float]] = None
434
  user: Optional[str] = None
435
- webSearchMode: Optional[bool] = False # Custom parameter
436
 
437
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
438
  return {
@@ -468,9 +529,7 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
468
  async_generator = Blackbox.create_async_generator(
469
  model=request.model,
470
  messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
471
- image=None,
472
- image_name=None,
473
- webSearchMode=request.webSearchMode
474
  )
475
 
476
  if request.stream:
@@ -478,7 +537,7 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
478
  try:
479
  async for chunk in async_generator:
480
  if isinstance(chunk, ImageResponse):
481
- image_markdown = f"![image]({chunk.url})"
482
  response_chunk = create_response(image_markdown, request.model)
483
  else:
484
  response_chunk = create_response(chunk, request.model)
@@ -499,7 +558,7 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
499
  response_content = ""
500
  async for chunk in async_generator:
501
  if isinstance(chunk, ImageResponse):
502
- response_content += f"![image]({chunk.url})\n"
503
  else:
504
  response_content += chunk
505
 
@@ -535,8 +594,6 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
535
  logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
536
  raise HTTPException(status_code=500, detail=str(e))
537
 
538
- # Re-added endpoints without API key authentication
539
-
540
  # Endpoint: POST /v1/tokenizer
541
  class TokenizerRequest(BaseModel):
542
  text: str
 
11
  from typing import List, Dict, Any, Optional, AsyncGenerator, Union
12
  from datetime import datetime
13
 
14
+ from aiohttp import ClientSession, ClientTimeout, ClientError, ClientResponseError
15
  from fastapi import FastAPI, HTTPException, Request, Depends, Header
16
  from fastapi.responses import StreamingResponse, JSONResponse, RedirectResponse
17
  from pydantic import BaseModel
 
46
  CLEANUP_INTERVAL = 60 # seconds
47
  RATE_LIMIT_WINDOW = 60 # seconds
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  class ImageResponse:
50
+ def __init__(self, images: str, alt: str):
51
+ self.images = images
52
  self.alt = alt
53
 
 
 
 
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
61
  supports_system_message = True
62
  supports_message_history = True
 
66
  models = [
67
  default_model,
68
  'blackboxai-pro',
69
+ *image_models,
70
  "llama-3.1-8b",
71
  'llama-3.1-70b',
72
  'llama-3.1-405b',
 
87
  'ReactAgent',
88
  'XcodeAgent',
89
  'AngularJSAgent',
 
 
90
  ]
91
 
 
 
 
 
92
  agentMode = {
93
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
 
94
  }
95
+
96
  trendingAgentMode = {
97
  "blackboxai": {},
98
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
 
140
  'AngularJSAgent': '@AngularJS Agent',
141
  'blackboxai-pro': '@BLACKBOXAI-PRO',
142
  'ImageGeneration': '@Image Generation',
 
143
  }
144
 
145
  model_referers = {
146
+ "blackboxai": "/?model=blackboxai",
147
+ "gpt-4o": "/?model=gpt-4o",
148
+ "gemini-pro": "/?model=gemini-pro",
149
+ "claude-sonnet-3.5": "/?model=claude-sonnet-3.5"
150
  }
151
 
152
  model_aliases = {
153
  "gemini-flash": "gemini-1.5-flash",
154
  "claude-3.5-sonnet": "claude-sonnet-3.5",
155
  "flux": "ImageGeneration",
 
156
  }
157
 
158
  @classmethod
159
+ def get_model(cls, model: str) -> str:
160
  if model in cls.models:
161
  return model
162
+ elif model in cls.model_aliases:
 
 
163
  return cls.model_aliases[model]
164
  else:
165
+ return cls.default_model
166
+
167
+ @staticmethod
168
+ def generate_random_string(length: int = 7) -> str:
169
+ characters = string.ascii_letters + string.digits
170
+ return ''.join(random.choices(characters, k=length))
171
+
172
+ @staticmethod
173
+ def generate_next_action() -> str:
174
+ return uuid.uuid4().hex
175
+
176
+ @staticmethod
177
+ def generate_next_router_state_tree() -> str:
178
+ router_state = [
179
+ "",
180
+ {
181
+ "children": [
182
+ "(chat)",
183
+ {
184
+ "children": [
185
+ "__PAGE__",
186
+ {}
187
+ ]
188
+ }
189
+ ]
190
+ },
191
+ None,
192
+ None,
193
+ True
194
+ ]
195
+ return json.dumps(router_state)
196
+
197
+ @staticmethod
198
+ def clean_response(text: str) -> str:
199
+ pattern = r'^\$\@\$v=undefined-rv1\$\@\$'
200
+ cleaned_text = re.sub(pattern, '', text)
201
+ return cleaned_text
202
 
203
  @classmethod
204
  async def create_async_generator(
 
206
  model: str,
207
  messages: List[Dict[str, str]],
208
  proxy: Optional[str] = None,
209
+ web_search_mode: bool = False,
 
 
210
  **kwargs
211
+ ) -> AsyncGenerator[Union[str, ImageResponse], None]:
212
+ """
213
+ Creates an asynchronous generator for streaming responses from Blackbox AI.
214
+ Parameters:
215
+ model (str): Model to use for generating responses.
216
+ messages (List[Dict[str, str]]): Message history.
217
+ proxy (Optional[str]): Proxy URL, if needed.
218
+ web_search_mode (bool): Enables or disables web search mode.
219
+ **kwargs: Additional keyword arguments.
220
+ Yields:
221
+ Union[str, ImageResponse]: Segments of the generated response or ImageResponse objects.
222
+ """
223
  model = cls.get_model(model)
224
  if model is None:
225
  logger.error(f"Model {model} is not available.")
226
  raise ModelNotWorkingException(model)
227
 
228
+ chat_id = cls.generate_random_string()
229
+ next_action = cls.generate_next_action()
230
+ next_router_state_tree = cls.generate_next_router_state_tree()
231
 
232
+ agent_mode = cls.agentMode.get(model, {})
233
+ trending_agent_mode = cls.trendingAgentMode.get(model, {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ prefix = cls.model_prefixes.get(model, "")
 
 
 
 
236
 
237
+ formatted_prompt = ""
238
+ for message in messages:
239
+ role = message.get('role', '').capitalize()
240
+ content = message.get('content', '')
241
+ if role and content:
242
+ formatted_prompt += f"{role}: {content}\n"
 
 
 
 
 
 
 
 
 
243
 
244
+ if prefix:
245
+ formatted_prompt = f"{prefix} {formatted_prompt}".strip()
246
+
247
+ referer_path = cls.model_referers.get(model, f"/?model={model}")
248
+ referer_url = f"{cls.url}{referer_path}"
249
+
250
+ common_headers = {
251
+ 'accept': '*/*',
252
+ 'accept-language': 'en-US,en;q=0.9',
253
+ 'cache-control': 'no-cache',
254
+ 'origin': cls.url,
255
+ 'pragma': 'no-cache',
256
+ 'priority': 'u=1, i',
257
+ 'sec-ch-ua': '"Chromium";v="129", "Not=A?Brand";v="8"',
258
+ 'sec-ch-ua-mobile': '?0',
259
+ 'sec-ch-ua-platform': '"Linux"',
260
+ 'sec-fetch-dest': 'empty',
261
+ 'sec-fetch-mode': 'cors',
262
+ 'sec-fetch-site': 'same-origin',
263
+ 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) '
264
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
265
+ 'Chrome/129.0.0.0 Safari/537.36'
266
+ }
267
+
268
+ headers_api_chat = {
269
+ 'Content-Type': 'application/json',
270
+ 'Referer': referer_url
271
+ }
272
+ headers_api_chat_combined = {**common_headers, **headers_api_chat}
273
+
274
+ payload_api_chat = {
275
+ "messages": [
276
+ {
277
+ "id": chat_id,
278
+ "content": formatted_prompt,
279
+ "role": "user"
280
+ }
281
+ ],
282
+ "id": chat_id,
283
  "previewToken": None,
284
  "userId": None,
285
  "codeModelMode": True,
286
+ "agentMode": agent_mode,
287
+ "trendingAgentMode": trending_agent_mode,
288
  "isMicMode": False,
289
  "userSystemPrompt": None,
290
+ "maxTokens": 1024,
291
  "playgroundTopP": 0.9,
292
  "playgroundTemperature": 0.5,
293
  "isChromeExt": False,
 
297
  "clickedForceWebSearch": False,
298
  "visitFromDelta": False,
299
  "mobileClient": False,
300
+ "webSearchMode": web_search_mode,
301
+ "userSelectedModel": cls.userSelectedModel.get(model, model)
302
  }
303
 
304
+ headers_chat = {
305
+ 'Accept': 'text/x-component',
306
+ 'Content-Type': 'text/plain;charset=UTF-8',
307
+ 'Referer': f'{cls.url}/chat/{chat_id}?model={model}',
308
+ 'next-action': next_action,
309
+ 'next-router-state-tree': next_router_state_tree,
310
+ 'next-url': '/'
311
+ }
312
+ headers_chat_combined = {**common_headers, **headers_chat}
313
 
314
+ data_chat = '[]'
 
315
 
316
+ async with ClientSession(headers=common_headers) as session:
317
  try:
318
+ async with session.post(
319
+ cls.api_endpoint,
320
+ headers=headers_api_chat_combined,
321
+ json=payload_api_chat,
322
+ proxy=proxy
323
+ ) as response_api_chat:
324
+ response_api_chat.raise_for_status()
325
+ text = await response_api_chat.text()
326
+ cleaned_response = cls.clean_response(text)
327
+
328
+ if model in cls.image_models:
329
+ match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_response)
330
+ if match:
331
+ image_url = match.group(1)
332
+ image_response = ImageResponse(images=image_url, alt="Generated Image")
333
+ yield image_response
334
+ else:
335
+ yield cleaned_response
336
+ else:
337
+ if web_search_mode:
338
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_response, re.DOTALL)
339
+ if match:
340
+ source_part = match.group(1).strip()
341
+ answer_part = cleaned_response[match.end():].strip()
342
+ try:
343
+ sources = json.loads(source_part)
344
+ source_formatted = "**Source:**\n"
345
+ for item in sources:
346
+ title = item.get('title', 'No Title')
347
+ link = item.get('link', '#')
348
+ position = item.get('position', '')
349
+ source_formatted += f"{position}. [{title}]({link})\n"
350
+ final_response = f"{answer_part}\n\n{source_formatted}"
351
+ except json.JSONDecodeError:
352
+ final_response = f"{answer_part}\n\nSource information is unavailable."
353
  else:
354
+ final_response = cleaned_response
 
355
  else:
356
+ if '$~~~$' in cleaned_response:
357
+ final_response = cleaned_response.split('$~~~$')[0].strip()
358
+ else:
359
+ final_response = cleaned_response
360
+
361
+ yield final_response
362
+ except ClientResponseError as e:
363
+ error_text = f"Error {e.status}: {e.message}"
364
+ try:
365
+ error_response = await e.response.text()
366
+ cleaned_error = cls.clean_response(error_response)
367
+ error_text += f" - {cleaned_error}"
368
+ except Exception:
369
+ pass
370
+ yield error_text
371
+ except Exception as e:
372
+ yield f"Unexpected error during /api/chat request: {str(e)}"
373
+
374
+ chat_url = f'{cls.url}/chat/{chat_id}?model={model}'
375
+
376
+ try:
377
+ async with session.post(
378
+ chat_url,
379
+ headers=headers_chat_combined,
380
+ data=data_chat,
381
+ proxy=proxy
382
+ ) as response_chat:
383
+ response_chat.raise_for_status()
384
+ pass
385
+ except ClientResponseError as e:
386
+ error_text = f"Error {e.status}: {e.message}"
387
+ try:
388
+ error_response = await e.response.text()
389
+ cleaned_error = cls.clean_response(error_response)
390
+ error_text += f" - {cleaned_error}"
391
+ except Exception:
392
+ pass
393
+ yield error_text
 
394
  except Exception as e:
395
+ yield f"Unexpected error during /chat/{chat_id} request: {str(e)}"
396
+
397
+ # Custom exception for model not working
398
+ class ModelNotWorkingException(Exception):
399
+ def __init__(self, model: str):
400
+ self.model = model
401
+ self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
402
+ super().__init__(self.message)
403
+
404
+ async def cleanup_rate_limit_stores():
405
+ """
406
+ Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
407
+ """
408
+ while True:
409
+ current_time = time.time()
410
+ ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
411
+ for ip in ips_to_delete:
412
+ del rate_limit_store[ip]
413
+ logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
414
+ await asyncio.sleep(CLEANUP_INTERVAL)
415
+
416
+ async def rate_limiter_per_ip(request: Request):
417
+ """
418
+ Rate limiter that enforces a limit based on the client's IP address.
419
+ """
420
+ client_ip = request.client.host
421
+ current_time = time.time()
422
+
423
+ # Initialize or update the count and timestamp
424
+ if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
425
+ rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
426
+ else:
427
+ if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
428
+ logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
429
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
430
+ rate_limit_store[client_ip]["count"] += 1
431
+
432
+ async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
433
+ """
434
+ Dependency to extract and validate the API key from the Authorization header.
435
+ """
436
+ client_ip = request.client.host
437
+ if authorization is None or not authorization.startswith('Bearer '):
438
+ logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
439
+ raise HTTPException(status_code=401, detail='Invalid authorization header format')
440
+ api_key = authorization[7:]
441
+ if api_key not in API_KEYS:
442
+ logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
443
+ raise HTTPException(status_code=401, detail='Invalid API key')
444
+ return api_key
445
 
446
  # FastAPI app setup
447
  app = FastAPI()
 
493
  frequency_penalty: Optional[float] = 0.0
494
  logit_bias: Optional[Dict[str, float]] = None
495
  user: Optional[str] = None
496
+ web_search_mode: Optional[bool] = False # Custom parameter
497
 
498
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
499
  return {
 
529
  async_generator = Blackbox.create_async_generator(
530
  model=request.model,
531
  messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
532
+ web_search_mode=request.web_search_mode
 
 
533
  )
534
 
535
  if request.stream:
 
537
  try:
538
  async for chunk in async_generator:
539
  if isinstance(chunk, ImageResponse):
540
+ image_markdown = f"![image]({chunk.images})"
541
  response_chunk = create_response(image_markdown, request.model)
542
  else:
543
  response_chunk = create_response(chunk, request.model)
 
558
  response_content = ""
559
  async for chunk in async_generator:
560
  if isinstance(chunk, ImageResponse):
561
+ response_content += f"![image]({chunk.images})\n"
562
  else:
563
  response_content += chunk
564
 
 
594
  logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
595
  raise HTTPException(status_code=500, detail=str(e))
596
 
 
 
597
  # Endpoint: POST /v1/tokenizer
598
  class TokenizerRequest(BaseModel):
599
  text: str