Niansuh commited on
Commit
0253f2a
·
verified ·
1 Parent(s): 836f559

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +219 -278
main.py CHANGED
@@ -8,14 +8,15 @@ 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, ClientResponseError
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(
@@ -29,6 +30,7 @@ logger = logging.getLogger(__name__)
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.")
@@ -53,10 +55,7 @@ async def cleanup_rate_limit_stores():
53
  """
54
  while True:
55
  current_time = time.time()
56
- ips_to_delete = [
57
- ip for ip, value in rate_limit_store.items()
58
- if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2
59
- ]
60
  for ip in ips_to_delete:
61
  del rate_limit_store[ip]
62
  logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
@@ -99,25 +98,47 @@ class ModelNotWorkingException(Exception):
99
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
100
  super().__init__(self.message)
101
 
102
- # ImageResponse class
103
  class ImageResponse:
104
  def __init__(self, url: str, alt: str):
105
  self.url = url
106
  self.alt = alt
107
 
108
- # Placeholder classes for AsyncGeneratorProvider and ProviderModelMixin
109
- class AsyncGeneratorProvider:
110
- pass # Implement as per your actual provider's requirements
111
-
112
- class ProviderModelMixin:
113
- pass # Implement as per your actual provider's requirements
114
-
115
- class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
116
- label = "Blackbox AI"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  url = "https://www.blackbox.ai"
118
  api_endpoint = "https://www.blackbox.ai/api/chat"
119
  working = True
120
- supports_gpt_4 = True
121
  supports_stream = True
122
  supports_system_message = True
123
  supports_message_history = True
@@ -127,7 +148,6 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
127
  models = [
128
  default_model,
129
  'blackboxai-pro',
130
- *image_models,
131
  "llama-3.1-8b",
132
  'llama-3.1-70b',
133
  'llama-3.1-405b',
@@ -148,6 +168,8 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
148
  'ReactAgent',
149
  'XcodeAgent',
150
  'AngularJSAgent',
 
 
151
  ]
152
 
153
  # Filter models based on AVAILABLE_MODELS
@@ -156,8 +178,8 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
156
 
157
  agentMode = {
158
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
 
159
  }
160
-
161
  trendingAgentMode = {
162
  "blackboxai": {},
163
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
@@ -184,7 +206,6 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
184
  "gpt-4o": "gpt-4o",
185
  "gemini-pro": "gemini-pro",
186
  'claude-sonnet-3.5': "claude-sonnet-3.5",
187
- "niansuh": "Niansuh", # Added based on model_aliases
188
  }
189
 
190
  model_prefixes = {
@@ -210,10 +231,10 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
210
  }
211
 
212
  model_referers = {
213
- "blackboxai": "/?model=blackboxai",
214
- "gpt-4o": "/?model=gpt-4o",
215
- "gemini-pro": "/?model=gemini-pro",
216
- "claude-sonnet-3.5": "/?model=claude-sonnet-3.5"
217
  }
218
 
219
  model_aliases = {
@@ -234,132 +255,89 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
234
  else:
235
  return cls.default_model if cls.default_model in cls.models else None
236
 
237
- @staticmethod
238
- def generate_random_string(length: int = 7) -> str:
239
- characters = string.ascii_letters + string.digits
240
- return ''.join(random.choices(characters, k=length))
241
-
242
- @staticmethod
243
- def generate_next_action() -> str:
244
- return uuid.uuid4().hex
245
-
246
- @staticmethod
247
- def generate_next_router_state_tree() -> str:
248
- router_state = [
249
- "",
250
- {
251
- "children": [
252
- "(chat)",
253
- {
254
- "children": [
255
- "__PAGE__",
256
- {}
257
- ]
258
- }
259
- ]
260
- },
261
- None,
262
- None,
263
- True
264
- ]
265
- return json.dumps(router_state)
266
-
267
- @staticmethod
268
- def clean_response(text: str) -> str:
269
- pattern = r'^\$\@\$v=undefined-rv1\$\@\$'
270
- cleaned_text = re.sub(pattern, '', text)
271
- return cleaned_text
272
-
273
  @classmethod
 
 
 
 
 
 
 
 
274
  async def create_async_generator(
275
  cls,
276
  model: str,
277
  messages: List[Dict[str, str]],
278
  proxy: Optional[str] = None,
279
- websearch: bool = False,
 
 
280
  **kwargs
281
- ) -> AsyncGenerator[Union[str, ImageResponse], None]:
282
  """
283
- Creates an asynchronous generator for streaming responses from Blackbox AI.
284
-
285
- Parameters:
286
- model (str): Model to use for generating responses.
287
- messages (List[Dict[str, str]]): Message history.
288
- proxy (Optional[str]): Proxy URL, if needed.
289
- websearch (bool): Enables or disables web search mode.
290
- **kwargs: Additional keyword arguments.
291
-
292
- Yields:
293
- Union[str, ImageResponse]: Segments of the generated response or ImageResponse objects.
294
  """
295
  model = cls.get_model(model)
296
  if model is None:
297
  logger.error(f"Model {model} is not available.")
298
  raise ModelNotWorkingException(model)
299
 
300
- chat_id = cls.generate_random_string()
301
- next_action = cls.generate_next_action()
302
- next_router_state_tree = cls.generate_next_router_state_tree()
303
-
304
- agent_mode = cls.agentMode.get(model, {})
305
- trending_agent_mode = cls.trendingAgentMode.get(model, {})
306
-
307
- prefix = cls.model_prefixes.get(model, "")
308
-
309
- formatted_prompt = ""
310
- for message in messages:
311
- role = message.get('role', '').capitalize()
312
- content = message.get('content', '')
313
- if role and content:
314
- formatted_prompt += f"{role}: {content}\n"
315
-
316
- if prefix:
317
- formatted_prompt = f"{prefix} {formatted_prompt}".strip()
318
-
319
- referer_path = cls.model_referers.get(model, f"/?model={model}")
320
- referer_url = f"{cls.url}{referer_path}"
321
-
322
- common_headers = {
323
- 'accept': '*/*',
324
- 'accept-language': 'en-US,en;q=0.9',
325
- 'cache-control': 'no-cache',
326
- 'origin': cls.url,
327
- 'pragma': 'no-cache',
328
- 'priority': 'u=1, i',
329
- 'sec-ch-ua': '"Chromium";v="129", "Not=A?Brand";v="8"',
330
- 'sec-ch-ua-mobile': '?0',
331
- 'sec-ch-ua-platform': '"Linux"',
332
- 'sec-fetch-dest': 'empty',
333
- 'sec-fetch-mode': 'cors',
334
- 'sec-fetch-site': 'same-origin',
335
- 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) '
336
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
337
- 'Chrome/129.0.0.0 Safari/537.36'
338
- }
339
 
340
- headers_api_chat = {
341
- 'Content-Type': 'application/json',
342
- 'Referer': referer_url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
- headers_api_chat_combined = {**common_headers, **headers_api_chat}
345
-
346
- payload_api_chat = {
347
- "messages": [
348
- {
349
- "id": chat_id,
350
- "content": formatted_prompt,
351
- "role": "user"
352
- }
353
- ],
354
- "id": chat_id,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  "previewToken": None,
356
  "userId": None,
357
  "codeModelMode": True,
358
- "agentMode": agent_mode,
359
- "trendingAgentMode": trending_agent_mode,
360
  "isMicMode": False,
361
  "userSystemPrompt": None,
362
- "maxTokens": 1024,
363
  "playgroundTopP": 0.9,
364
  "playgroundTemperature": 0.5,
365
  "isChromeExt": False,
@@ -369,111 +347,101 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
369
  "clickedForceWebSearch": False,
370
  "visitFromDelta": False,
371
  "mobileClient": False,
372
- "webSearchMode": websearch,
373
- "userSelectedModel": cls.userSelectedModel.get(model, model)
374
- }
375
-
376
- headers_chat = {
377
- 'Accept': 'text/x-component',
378
- 'Content-Type': 'text/plain;charset=UTF-8',
379
- 'Referer': f'{cls.url}/chat/{chat_id}?model={model}',
380
- 'next-action': next_action,
381
- 'next-router-state-tree': next_router_state_tree,
382
- 'next-url': '/'
383
  }
384
- headers_chat_combined = {**common_headers, **headers_chat}
385
-
386
- data_chat = '[]'
387
 
388
- async with ClientSession(headers=common_headers) as session:
389
- try:
390
- # Send initial chat request with streaming
391
- async with session.post(
392
- cls.api_endpoint,
393
- headers=headers_api_chat_combined,
394
- json=payload_api_chat,
395
- proxy=proxy,
396
- timeout=ClientTimeout(total=600), # Adjust timeout as needed
397
- ) as response_api_chat:
398
- response_api_chat.raise_for_status()
399
-
400
- # Stream the response in chunks
401
- async for data in response_api_chat.content.iter_chunked(1024):
402
- decoded_data = data.decode('utf-8', errors='ignore')
403
- cleaned_data = cls.clean_response(decoded_data)
404
-
405
- # Check for image response
406
- image_match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_data)
407
- if image_match:
408
- image_url = image_match.group(1)
409
- image_response = ImageResponse(url=image_url, alt="Generated Image")
410
- yield image_response
411
- continue # Continue to the next chunk
412
-
413
- # Check for web search sources
414
- if websearch:
415
- source_match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_data, re.DOTALL)
416
- if source_match:
417
- source_part = source_match.group(1).strip()
418
- answer_part = cleaned_data[source_match.end():].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  try:
420
- sources = json.loads(source_part)
421
- source_formatted = "**Sources:**\n"
422
- for item in sources[:5]:
423
- title = item.get('title', 'No Title')
424
- link = item.get('link', '#')
425
- position = item.get('position', '')
426
- source_formatted += f"{position}. [{title}]({link})\n"
427
- final_response = f"{answer_part}\n\n{source_formatted}"
428
- except json.JSONDecodeError:
429
- final_response = f"{answer_part}\n\nSource information is unavailable."
430
- yield final_response
431
- continue # Continue to the next chunk
432
-
433
- # Yield the cleaned data chunk
434
- if cleaned_data.strip():
435
- yield cleaned_data.strip()
436
- except ClientResponseError as e:
437
- error_text = f"Error {e.status}: {e.message}"
438
- try:
439
- error_response = await e.response.text()
440
- cleaned_error = cls.clean_response(error_response)
441
- error_text += f" - {cleaned_error}"
442
- except Exception:
443
- pass
444
- yield error_text
445
- except Exception as e:
446
- yield f"Unexpected error during /api/chat request: {str(e)}"
447
 
448
- chat_url = f'{cls.url}/chat/{chat_id}?model={model}'
 
 
 
 
449
 
450
- try:
451
- # Send follow-up chat request (if necessary)
452
- async with session.post(
453
- chat_url,
454
- headers=headers_chat_combined,
455
- data=data_chat,
456
- proxy=proxy,
457
- timeout=ClientTimeout(total=600),
458
- ) as response_chat:
459
- response_chat.raise_for_status()
460
- # If there's additional streaming data from the chat URL, handle it here
461
- async for data in response_chat.content.iter_chunked(1024):
462
- decoded_data = data.decode('utf-8', errors='ignore')
463
- cleaned_data = cls.clean_response(decoded_data)
464
- if cleaned_data.strip():
465
- yield cleaned_data.strip()
466
- except ClientResponseError as e:
467
- error_text = f"Error {e.status}: {e.message}"
468
- try:
469
- error_response = await e.response.text()
470
- cleaned_error = cls.clean_response(error_response)
471
- error_text += f" - {cleaned_error}"
472
- except Exception:
473
- pass
474
- yield error_text
475
- except Exception as e:
476
- yield f"Unexpected error during /chat/{chat_id} request: {str(e)}"
477
 
478
  # Request Models
479
  class Message(BaseModel):
@@ -526,40 +494,6 @@ def create_response(content: str, model: str, finish_reason: Optional[str] = Non
526
  "usage": None, # To be filled in non-streaming responses
527
  }
528
 
529
- # Initialize FastAPI app
530
- app = FastAPI()
531
-
532
- # Add the cleanup task when the app starts
533
- @app.on_event("startup")
534
- async def startup_event():
535
- asyncio.create_task(cleanup_rate_limit_stores())
536
- logger.info("Started rate limit store cleanup task.")
537
-
538
- # Middleware to enhance security and enforce Content-Type for specific endpoints
539
- @app.middleware("http")
540
- async def security_middleware(request: Request, call_next):
541
- client_ip = request.client.host
542
- # Enforce that POST requests to /v1/chat/completions must have Content-Type: application/json
543
- if request.method == "POST" and request.url.path == "/v1/chat/completions":
544
- content_type = request.headers.get("Content-Type")
545
- if content_type != "application/json":
546
- logger.warning(f"Invalid Content-Type from IP: {client_ip} for path: {request.url.path}")
547
- return JSONResponse(
548
- status_code=400,
549
- content={
550
- "error": {
551
- "message": "Content-Type must be application/json",
552
- "type": "invalid_request_error",
553
- "param": None,
554
- "code": None
555
- }
556
- },
557
- )
558
- response = await call_next(request)
559
- return response
560
-
561
- # FastAPI Endpoints
562
-
563
  @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
564
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
565
  client_ip = req.client.host
@@ -578,28 +512,24 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
578
  async_generator = Blackbox.create_async_generator(
579
  model=request.model,
580
  messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
581
- proxy=None, # Add proxy if needed
582
- websearch=request.webSearchMode
 
583
  )
584
 
585
  if request.stream:
586
  async def generate():
587
  try:
588
  assistant_content = ""
589
- prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
590
- completion_tokens = 0
591
-
592
  async for chunk in async_generator:
593
  if isinstance(chunk, ImageResponse):
594
  # Handle image responses if necessary
595
- image_markdown = f"![{chunk.alt}]({chunk.url})\n"
596
  assistant_content += image_markdown
597
  response_chunk = create_response(image_markdown, request.model, finish_reason=None)
598
- yield f"data: {json.dumps(response_chunk)}\n\n"
599
  else:
600
- # Assuming 'chunk' is a string of text
601
  assistant_content += chunk
602
- completion_tokens += len(chunk.split())
603
  response_chunk = {
604
  "id": f"chatcmpl-{uuid.uuid4()}",
605
  "object": "chat.completion.chunk",
@@ -614,9 +544,11 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
614
  ],
615
  "usage": None, # Usage can be updated if you track tokens in real-time
616
  }
617
- yield f"data: {json.dumps(response_chunk)}\n\n"
618
-
619
- # After all chunks are sent, calculate tokens and estimated cost
 
 
620
  total_tokens = prompt_tokens + completion_tokens
621
  estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
622
 
@@ -657,7 +589,7 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
657
  response_content = ""
658
  async for chunk in async_generator:
659
  if isinstance(chunk, ImageResponse):
660
- response_content += f"![{chunk.alt}]({chunk.url})\n"
661
  else:
662
  response_content += chunk
663
 
@@ -705,9 +637,18 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
705
  async def tokenizer(request: TokenizerRequest, req: Request):
706
  client_ip = req.client.host
707
  text = request.text
708
- token_count = len(text.split())
709
  logger.info(f"Tokenizer requested from IP: {client_ip} | Text length: {len(text)}")
710
- return {"text": text, "tokens": token_count}
 
 
 
 
 
 
 
 
 
 
711
 
712
  # Endpoint: GET /v1/models
713
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
 
8
  import asyncio
9
  import time
10
  from collections import defaultdict
11
+ from typing import List, Dict, Any, Optional, AsyncGenerator, Union, Callable, Type, Tuple
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
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type, RetryError
20
 
21
  # Configure logging
22
  logging.basicConfig(
 
30
  API_KEYS = os.getenv('API_KEYS', '').split(',') # Comma-separated API keys
31
  RATE_LIMIT = int(os.getenv('RATE_LIMIT', '60')) # Requests per minute
32
  AVAILABLE_MODELS = os.getenv('AVAILABLE_MODELS', '') # Comma-separated available models
33
+ RETRY_ATTEMPTS = int(os.getenv('RETRY_ATTEMPTS', '5')) # Retry attempts
34
 
35
  if not API_KEYS or API_KEYS == ['']:
36
  logger.error("No API keys found. Please set the API_KEYS environment variable.")
 
55
  """
56
  while True:
57
  current_time = time.time()
58
+ ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
 
 
 
59
  for ip in ips_to_delete:
60
  del rate_limit_store[ip]
61
  logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
 
98
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
99
  super().__init__(self.message)
100
 
101
+ # Mock implementations for ImageResponse and to_data_uri
102
  class ImageResponse:
103
  def __init__(self, url: str, alt: str):
104
  self.url = url
105
  self.alt = alt
106
 
107
+ def to_data_uri(image: Any) -> str:
108
+ return "data:image/png;base64,..." # Replace with actual base64 data
109
+
110
+ # Retry Decorator
111
+ def async_retry(
112
+ retries: int = 5,
113
+ exceptions: Tuple[Type[BaseException], ...] = (ClientError, asyncio.TimeoutError),
114
+ initial_delay: float = 1.0,
115
+ max_delay: float = 10.0,
116
+ backoff_multiplier: float = 2.0,
117
+ jitter: float = 0.1,
118
+ ) -> Callable:
119
+ """
120
+ Asynchronous retry decorator with exponential backoff and jitter.
121
+ """
122
+ def decorator(func: Callable) -> Callable:
123
+ @retry(
124
+ stop=stop_after_attempt(retries),
125
+ wait=wait_exponential(multiplier=initial_delay, min=initial_delay, max=max_delay) + wait_exponential(multiplier=0, max=jitter),
126
+ retry=retry_if_exception_type(exceptions),
127
+ reraise=True,
128
+ )
129
+ async def wrapper(*args, **kwargs):
130
+ try:
131
+ return await func(*args, **kwargs)
132
+ except exceptions as e:
133
+ logger.warning(f"Function {func.__name__} failed with {e}. Retrying...")
134
+ raise
135
+ return wrapper
136
+ return decorator
137
+
138
+ class Blackbox:
139
  url = "https://www.blackbox.ai"
140
  api_endpoint = "https://www.blackbox.ai/api/chat"
141
  working = True
 
142
  supports_stream = True
143
  supports_system_message = True
144
  supports_message_history = True
 
148
  models = [
149
  default_model,
150
  'blackboxai-pro',
 
151
  "llama-3.1-8b",
152
  'llama-3.1-70b',
153
  'llama-3.1-405b',
 
168
  'ReactAgent',
169
  'XcodeAgent',
170
  'AngularJSAgent',
171
+ *image_models,
172
+ 'Niansuh',
173
  ]
174
 
175
  # Filter models based on AVAILABLE_MODELS
 
178
 
179
  agentMode = {
180
  'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
181
+ 'Niansuh': {'mode': True, 'id': "NiansuhAIk1HgESy", 'name': "Niansuh"},
182
  }
 
183
  trendingAgentMode = {
184
  "blackboxai": {},
185
  "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
 
206
  "gpt-4o": "gpt-4o",
207
  "gemini-pro": "gemini-pro",
208
  'claude-sonnet-3.5': "claude-sonnet-3.5",
 
209
  }
210
 
211
  model_prefixes = {
 
231
  }
232
 
233
  model_referers = {
234
+ "blackboxai": f"{url}/?model=blackboxai",
235
+ "gpt-4o": f"{url}/?model=gpt-4o",
236
+ "gemini-pro": f"{url}/?model=gemini-pro",
237
+ "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
238
  }
239
 
240
  model_aliases = {
 
255
  else:
256
  return cls.default_model if cls.default_model in cls.models else None
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  @classmethod
259
+ @async_retry(
260
+ retries=RETRY_ATTEMPTS,
261
+ exceptions=(ClientError, asyncio.TimeoutError),
262
+ initial_delay=1.0,
263
+ max_delay=10.0,
264
+ backoff_multiplier=2.0,
265
+ jitter=0.1,
266
+ )
267
  async def create_async_generator(
268
  cls,
269
  model: str,
270
  messages: List[Dict[str, str]],
271
  proxy: Optional[str] = None,
272
+ image: Any = None,
273
+ image_name: Optional[str] = None,
274
+ webSearchMode: bool = False,
275
  **kwargs
276
+ ) -> AsyncGenerator[Any, None]:
277
  """
278
+ Create an asynchronous generator to interact with the external API.
 
 
 
 
 
 
 
 
 
 
279
  """
280
  model = cls.get_model(model)
281
  if model is None:
282
  logger.error(f"Model {model} is not available.")
283
  raise ModelNotWorkingException(model)
284
 
285
+ logger.info(f"Selected model: {model}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
+ if not cls.working or model not in cls.models:
288
+ logger.error(f"Model {model} is not working or not supported.")
289
+ raise ModelNotWorkingException(model)
290
+
291
+ headers = {
292
+ "accept": "*/*",
293
+ "accept-language": "en-US,en;q=0.9",
294
+ "cache-control": "no-cache",
295
+ "content-type": "application/json",
296
+ "origin": cls.url,
297
+ "pragma": "no-cache",
298
+ "priority": "u=1, i",
299
+ "referer": cls.model_referers.get(model, cls.url),
300
+ "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"',
301
+ "sec-ch-ua-mobile": "?0",
302
+ "sec-ch-ua-platform": '"Linux"',
303
+ "sec-fetch-dest": "empty",
304
+ "sec-fetch-mode": "cors",
305
+ "sec-fetch-site": "same-origin",
306
+ "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
307
  }
308
+
309
+ if model in cls.model_prefixes:
310
+ prefix = cls.model_prefixes[model]
311
+ if not messages[0]['content'].startswith(prefix):
312
+ logger.debug(f"Adding prefix '{prefix}' to the first message.")
313
+ messages[0]['content'] = f"{prefix} {messages[0]['content']}"
314
+
315
+ random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
316
+ messages[-1]['id'] = random_id
317
+ messages[-1]['role'] = 'user'
318
+
319
+ logger.debug(f"Generated message ID: {random_id} for model: {model}")
320
+
321
+ if image is not None:
322
+ messages[-1]['data'] = {
323
+ 'fileText': '',
324
+ 'imageBase64': to_data_uri(image),
325
+ 'title': image_name
326
+ }
327
+ messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
328
+ logger.debug("Image data added to the message.")
329
+
330
+ data = {
331
+ "messages": messages,
332
+ "id": random_id,
333
  "previewToken": None,
334
  "userId": None,
335
  "codeModelMode": True,
336
+ "agentMode": {},
337
+ "trendingAgentMode": {},
338
  "isMicMode": False,
339
  "userSystemPrompt": None,
340
+ "maxTokens": 99999999,
341
  "playgroundTopP": 0.9,
342
  "playgroundTemperature": 0.5,
343
  "isChromeExt": False,
 
347
  "clickedForceWebSearch": False,
348
  "visitFromDelta": False,
349
  "mobileClient": False,
350
+ "userSelectedModel": None,
351
+ "webSearchMode": webSearchMode,
 
 
 
 
 
 
 
 
 
352
  }
 
 
 
353
 
354
+ if model in cls.agentMode:
355
+ data["agentMode"] = cls.agentMode[model]
356
+ elif model in cls.trendingAgentMode:
357
+ data["trendingAgentMode"] = cls.trendingAgentMode[model]
358
+ elif model in cls.userSelectedModel:
359
+ data["userSelectedModel"] = cls.userSelectedModel[model]
360
+ logger.info(f"Sending request to {cls.api_endpoint} with data (excluding messages).")
361
+
362
+ timeout = ClientTimeout(total=60) # Set an appropriate timeout
363
+
364
+ try:
365
+ async with ClientSession(headers=headers, timeout=timeout) as session:
366
+ async with session.post(cls.api_endpoint, json=data, proxy=proxy) as response:
367
+ response.raise_for_status()
368
+ logger.info(f"Received response with status {response.status}")
369
+ if model == 'ImageGeneration':
370
+ response_text = await response.text()
371
+ url_match = re.search(r'https://storage\.googleapis\.com/[^\s\)]+', response_text)
372
+ if url_match:
373
+ image_url = url_match.group(0)
374
+ logger.info(f"Image URL found.")
375
+ yield ImageResponse(image_url, alt=messages[-1]['content'])
376
+ else:
377
+ logger.error("Image URL not found in the response.")
378
+ raise Exception("Image URL not found in the response")
379
+ else:
380
+ full_response = ""
381
+ search_results_json = ""
382
+ try:
383
+ async for chunk, _ in response.content.iter_chunks():
384
+ if chunk:
385
+ decoded_chunk = chunk.decode(errors='ignore')
386
+ decoded_chunk = re.sub(r'\$@\$v=[^$]+\$@\$', '', decoded_chunk)
387
+ if decoded_chunk.strip():
388
+ if '$~~~$' in decoded_chunk:
389
+ search_results_json += decoded_chunk
390
+ else:
391
+ full_response += decoded_chunk
392
+ yield decoded_chunk
393
+ logger.info("Finished streaming response chunks.")
394
+ except Exception as e:
395
+ logger.exception("Error while iterating over response chunks.")
396
+ raise e
397
+ if data["webSearchMode"] and search_results_json:
398
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', search_results_json, re.DOTALL)
399
+ if match:
400
  try:
401
+ search_results = json.loads(match.group(1))
402
+ formatted_results = "\n\n**Sources:**\n"
403
+ for i, result in enumerate(search_results[:5], 1):
404
+ formatted_results += f"{i}. [{result['title']}]({result['link']})\n"
405
+ logger.info("Formatted search results.")
406
+ yield formatted_results
407
+ except json.JSONDecodeError as je:
408
+ logger.error("Failed to parse search results JSON.")
409
+ raise je
410
+ except RetryError as re:
411
+ logger.error(f"All retry attempts failed for {cls.api_endpoint}: {re}")
412
+ raise HTTPException(status_code=502, detail="Error communicating with the external API.")
413
+
414
+ # FastAPI app setup
415
+ app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
+ # Add the cleanup task when the app starts
418
+ @app.on_event("startup")
419
+ async def startup_event():
420
+ asyncio.create_task(cleanup_rate_limit_stores())
421
+ logger.info("Started rate limit store cleanup task.")
422
 
423
+ # Middleware to enhance security and enforce Content-Type for specific endpoints
424
+ @app.middleware("http")
425
+ async def security_middleware(request: Request, call_next):
426
+ client_ip = request.client.host
427
+ # Enforce that POST requests to /v1/chat/completions must have Content-Type: application/json
428
+ if request.method == "POST" and request.url.path == "/v1/chat/completions":
429
+ content_type = request.headers.get("Content-Type")
430
+ if content_type != "application/json":
431
+ logger.warning(f"Invalid Content-Type from IP: {client_ip} for path: {request.url.path}")
432
+ return JSONResponse(
433
+ status_code=400,
434
+ content={
435
+ "error": {
436
+ "message": "Content-Type must be application/json",
437
+ "type": "invalid_request_error",
438
+ "param": None,
439
+ "code": None
440
+ }
441
+ },
442
+ )
443
+ response = await call_next(request)
444
+ return response
 
 
 
 
 
445
 
446
  # Request Models
447
  class Message(BaseModel):
 
494
  "usage": None, # To be filled in non-streaming responses
495
  }
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
498
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
499
  client_ip = req.client.host
 
512
  async_generator = Blackbox.create_async_generator(
513
  model=request.model,
514
  messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
515
+ image=None,
516
+ image_name=None,
517
+ webSearchMode=request.webSearchMode
518
  )
519
 
520
  if request.stream:
521
  async def generate():
522
  try:
523
  assistant_content = ""
 
 
 
524
  async for chunk in async_generator:
525
  if isinstance(chunk, ImageResponse):
526
  # Handle image responses if necessary
527
+ image_markdown = f"![image]({chunk.url})\n"
528
  assistant_content += image_markdown
529
  response_chunk = create_response(image_markdown, request.model, finish_reason=None)
 
530
  else:
 
531
  assistant_content += chunk
532
+ # Yield the chunk as a partial choice
533
  response_chunk = {
534
  "id": f"chatcmpl-{uuid.uuid4()}",
535
  "object": "chat.completion.chunk",
 
544
  ],
545
  "usage": None, # Usage can be updated if you track tokens in real-time
546
  }
547
+ yield f"data: {json.dumps(response_chunk)}\n\n"
548
+
549
+ # After all chunks are sent, send the final message with finish_reason
550
+ prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
551
+ completion_tokens = len(assistant_content.split())
552
  total_tokens = prompt_tokens + completion_tokens
553
  estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
554
 
 
589
  response_content = ""
590
  async for chunk in async_generator:
591
  if isinstance(chunk, ImageResponse):
592
+ response_content += f"![image]({chunk.url})\n"
593
  else:
594
  response_content += chunk
595
 
 
637
  async def tokenizer(request: TokenizerRequest, req: Request):
638
  client_ip = req.client.host
639
  text = request.text
 
640
  logger.info(f"Tokenizer requested from IP: {client_ip} | Text length: {len(text)}")
641
+
642
+ try:
643
+ # Example integration: Assuming Blackbox has a tokenizer endpoint
644
+ result = await Blackbox.process_tokenizer_request(text)
645
+ token_count = result.get("tokens", len(text.split()))
646
+ return {"text": text, "tokens": token_count}
647
+ except HTTPException as he:
648
+ raise he
649
+ except Exception as e:
650
+ logger.exception(f"An unexpected error occurred during tokenization from IP: {client_ip}.")
651
+ raise HTTPException(status_code=500, detail=str(e))
652
 
653
  # Endpoint: GET /v1/models
654
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])