Niansuh commited on
Commit
479563b
·
verified ·
1 Parent(s): 05f6d1c

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +139 -392
main.py CHANGED
@@ -1,4 +1,3 @@
1
- import os
2
  import re
3
  import random
4
  import string
@@ -6,21 +5,13 @@ import uuid
6
  import json
7
  import logging
8
  import asyncio
 
9
  from aiohttp import ClientSession, ClientTimeout, ClientError
10
- from fastapi import FastAPI, HTTPException, Request, Depends, Header, status
11
- from fastapi.responses import StreamingResponse, JSONResponse
12
- from fastapi.middleware.cors import CORSMiddleware
13
- from pydantic import BaseModel, Field, validator
14
- from typing import List, Dict, Any, Optional, Union, AsyncGenerator, Literal
15
  from datetime import datetime
16
- from slowapi import Limiter, _rate_limit_exceeded_handler
17
- from slowapi.util import get_remote_address
18
- from slowapi.errors import RateLimitExceeded
19
- import tiktoken
20
- from dotenv import load_dotenv
21
-
22
- # Load environment variables from .env file
23
- load_dotenv()
24
 
25
  # Configure logging
26
  logging.basicConfig(
@@ -32,57 +23,6 @@ logging.basicConfig(
32
  )
33
  logger = logging.getLogger(__name__)
34
 
35
- # Initialize FastAPI app
36
- app = FastAPI(title="OpenAI-Compatible API")
37
-
38
- # Configure CORS (adjust origins as needed)
39
- origins = [
40
- "*", # Allow all origins; replace with specific origins in production
41
- ]
42
-
43
- app.add_middleware(
44
- CORSMiddleware,
45
- allow_origins=origins,
46
- allow_credentials=True,
47
- allow_methods=["*"],
48
- allow_headers=["*"],
49
- )
50
-
51
- # Initialize Rate Limiter from environment variable
52
- RATE_LIMIT = os.getenv("RATE_LIMIT", "60/minute") # Default to 60 requests per minute
53
- limiter = Limiter(key_func=get_remote_address, default_limits=[RATE_LIMIT])
54
- app.state.limiter = limiter
55
- app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
56
-
57
- # API Key Authentication
58
- API_KEYS = set(api_key.strip() for api_key in os.getenv("API_KEYS", "").split(",") if api_key.strip())
59
-
60
- async def get_api_key(authorization: Optional[str] = Header(None)):
61
- """
62
- Dependency to validate API Key from the Authorization header.
63
- """
64
- if authorization is None:
65
- raise HTTPException(
66
- status_code=status.HTTP_401_UNAUTHORIZED,
67
- detail="Authorization header missing",
68
- headers={"WWW-Authenticate": "Bearer"},
69
- )
70
- parts = authorization.split()
71
- if parts[0].lower() != "bearer" or len(parts) != 2:
72
- raise HTTPException(
73
- status_code=status.HTTP_401_UNAUTHORIZED,
74
- detail="Invalid authorization header format",
75
- headers={"WWW-Authenticate": "Bearer"},
76
- )
77
- token = parts[1]
78
- if token not in API_KEYS:
79
- raise HTTPException(
80
- status_code=status.HTTP_401_UNAUTHORIZED,
81
- detail="Invalid API Key",
82
- headers={"WWW-Authenticate": "Bearer"},
83
- )
84
- return token
85
-
86
  # Custom exception for model not working
87
  class ModelNotWorkingException(Exception):
88
  def __init__(self, model: str):
@@ -90,162 +30,34 @@ class ModelNotWorkingException(Exception):
90
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
91
  super().__init__(self.message)
92
 
93
- # Mock implementations for ImageResponse and to_data_uri (custom functionality)
94
  class ImageResponse:
95
- def __init__(self, url: str, alt: str):
96
- self.url = url
97
  self.alt = alt
98
 
99
- def to_data_uri(image: Any) -> str:
100
- return "data:image/png;base64,..." # Replace with actual base64 data if needed
 
101
 
102
- # Token Counting using tiktoken
103
- def count_tokens(messages: List[Dict[str, Any]], model: str) -> int:
104
- """
105
- Counts the number of tokens in the messages using tiktoken.
106
- Adjust the encoding based on the model.
107
- """
108
  try:
109
- encoding = tiktoken.get_encoding("cl100k_base") # Adjust encoding as per model
110
- except:
111
- encoding = tiktoken.get_encoding("cl100k_base") # Default encoding
112
- tokens = 0
113
- for message in messages:
114
- if isinstance(message['content'], list):
115
- for content_part in message['content']:
116
- if isinstance(content_part, dict):
117
- if content_part.get('type') == 'text':
118
- tokens += len(encoding.encode(content_part['text']))
119
- elif content_part.get('type') == 'image_url':
120
- tokens += len(encoding.encode(content_part['image_url']['url']))
121
- else:
122
- tokens += len(encoding.encode(message['content']))
123
- return tokens
124
 
125
- # Blackbox Class: Handles interaction with the external AI service
126
  class Blackbox:
127
- url = "https://www.blackbox.ai"
128
- api_endpoint = os.getenv("EXTERNAL_API_ENDPOINT", "https://www.blackbox.ai/api/chat")
129
- working = True
130
- supports_stream = True
131
- supports_system_message = True
132
- supports_message_history = True
133
-
134
- default_model = 'blackboxai'
135
- image_models = ['ImageGeneration']
136
- models = [
137
- default_model,
138
- 'blackboxai-pro',
139
- "llama-3.1-8b",
140
- 'llama-3.1-70b',
141
- 'llama-3.1-405b',
142
- 'gpt-4o',
143
- 'gemini-pro',
144
- 'gemini-1.5-flash',
145
- 'claude-sonnet-3.5',
146
- 'PythonAgent',
147
- 'JavaAgent',
148
- 'JavaScriptAgent',
149
- 'HTMLAgent',
150
- 'GoogleCloudAgent',
151
- 'AndroidDeveloper',
152
- 'SwiftDeveloper',
153
- 'Next.jsAgent',
154
- 'MongoDBAgent',
155
- 'PyTorchAgent',
156
- 'ReactAgent',
157
- 'XcodeAgent',
158
- 'AngularJSAgent',
159
- *image_models,
160
- 'Niansuh',
161
- ]
162
-
163
- agentMode = {
164
- 'ImageGeneration': {'mode': True, 'id': "ImageGenerationLV45LJp", 'name': "Image Generation"},
165
- 'Niansuh': {'mode': True, 'id': "NiansuhAIk1HgESy", 'name': "Niansuh"},
166
- }
167
- trendingAgentMode = {
168
- "blackboxai": {},
169
- "gemini-1.5-flash": {'mode': True, 'id': 'Gemini'},
170
- "llama-3.1-8b": {'mode': True, 'id': "llama-3.1-8b"},
171
- 'llama-3.1-70b': {'mode': True, 'id': "llama-3.1-70b"},
172
- 'llama-3.1-405b': {'mode': True, 'id': "llama-3.1-405b"},
173
- 'blackboxai-pro': {'mode': True, 'id': "BLACKBOXAI-PRO"},
174
- 'PythonAgent': {'mode': True, 'id': "Python Agent"},
175
- 'JavaAgent': {'mode': True, 'id': "Java Agent"},
176
- 'JavaScriptAgent': {'mode': True, 'id': "JavaScript Agent"},
177
- 'HTMLAgent': {'mode': True, 'id': "HTML Agent"},
178
- 'GoogleCloudAgent': {'mode': True, 'id': "Google Cloud Agent"},
179
- 'AndroidDeveloper': {'mode': True, 'id': "Android Developer"},
180
- 'SwiftDeveloper': {'mode': True, 'id': "Swift Developer"},
181
- 'Next.jsAgent': {'mode': True, 'id': "Next.js Agent"},
182
- 'MongoDBAgent': {'mode': True, 'id': "MongoDB Agent"},
183
- 'PyTorchAgent': {'mode': True, 'id': "PyTorch Agent"},
184
- 'ReactAgent': {'mode': True, 'id': "React Agent"},
185
- 'XcodeAgent': {'mode': True, 'id': "Xcode Agent"},
186
- 'AngularJSAgent': {'mode': True, 'id': "AngularJS Agent"},
187
- }
188
-
189
- userSelectedModel = {
190
- "gpt-4o": "gpt-4o",
191
- "gemini-pro": "gemini-pro",
192
- 'claude-sonnet-3.5': "claude-sonnet-3.5",
193
- }
194
-
195
- model_prefixes = {
196
- 'gpt-4o': '@GPT-4o',
197
- 'gemini-pro': '@Gemini-PRO',
198
- 'claude-sonnet-3.5': '@Claude-Sonnet-3.5',
199
- 'PythonAgent': '@Python Agent',
200
- 'JavaAgent': '@Java Agent',
201
- 'JavaScriptAgent': '@JavaScript Agent',
202
- 'HTMLAgent': '@HTML Agent',
203
- 'GoogleCloudAgent': '@Google Cloud Agent',
204
- 'AndroidDeveloper': '@Android Developer',
205
- 'SwiftDeveloper': '@Swift Developer',
206
- 'Next.jsAgent': '@Next.js Agent',
207
- 'MongoDBAgent': '@MongoDB Agent',
208
- 'PyTorchAgent': '@PyTorch Agent',
209
- 'ReactAgent': '@React Agent',
210
- 'XcodeAgent': '@Xcode Agent',
211
- 'AngularJSAgent': '@AngularJS Agent',
212
- 'blackboxai-pro': '@BLACKBOXAI-PRO',
213
- 'ImageGeneration': '@Image Generation',
214
- 'Niansuh': '@Niansuh',
215
- }
216
-
217
- model_referers = {
218
- "blackboxai": f"{url}/?model=blackboxai",
219
- "gpt-4o": f"{url}/?model=gpt-4o",
220
- "gemini-pro": f"{url}/?model=gemini-pro",
221
- "claude-sonnet-3.5": f"{url}/?model=claude-sonnet-3.5"
222
- }
223
-
224
- model_aliases = {
225
- "gemini-flash": "gemini-1.5-flash",
226
- "claude-3.5-sonnet": "claude-sonnet-3.5",
227
- "flux": "ImageGeneration",
228
- "niansuh": "Niansuh",
229
- }
230
-
231
- @classmethod
232
- def get_model(cls, model: str) -> str:
233
- if model in cls.models:
234
- return model
235
- elif model in cls.userSelectedModel:
236
- return model
237
- elif model in cls.model_aliases:
238
- return cls.model_aliases[model]
239
- else:
240
- return cls.default_model
241
 
242
  @classmethod
243
  async def create_async_generator(
244
  cls,
245
  model: str,
246
- messages: List[Dict[str, Any]],
247
  proxy: Optional[str] = None,
248
- image: Any = None,
249
  image_name: Optional[str] = None,
250
  webSearchMode: bool = False,
251
  **kwargs
@@ -258,52 +70,34 @@ class Blackbox:
258
  raise ModelNotWorkingException(model)
259
 
260
  headers = {
261
- "accept": "*/*",
262
- "accept-language": "en-US,en;q=0.9",
263
- "cache-control": "no-cache",
264
- "content-type": "application/json",
265
- "origin": cls.url,
266
- "pragma": "no-cache",
267
- "priority": "u=1, i",
268
- "referer": cls.model_referers.get(model, cls.url),
269
- "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"',
270
- "sec-ch-ua-mobile": "?0",
271
- "sec-ch-ua-platform": '"Linux"',
272
- "sec-fetch-dest": "empty",
273
- "sec-fetch-mode": "cors",
274
- "sec-fetch-site": "same-origin",
275
- "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
276
  }
277
 
278
  if model in cls.model_prefixes:
279
  prefix = cls.model_prefixes[model]
280
- if messages and isinstance(messages[0]['content'], list):
281
- # Prepend prefix to the first text message
282
- for content_part in messages[0]['content']:
283
- if isinstance(content_part, dict) and content_part.get('type') == 'text' and not content_part['text'].startswith(prefix):
284
- logger.debug(f"Adding prefix '{prefix}' to the first text message.")
285
- content_part['text'] = f"{prefix} {content_part['text']}"
286
- break
287
- elif messages and isinstance(messages[0]['content'], str) and not messages[0]['content'].startswith(prefix):
288
  messages[0]['content'] = f"{prefix} {messages[0]['content']}"
289
-
290
  random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
291
- # Assuming the last message is from the user
292
- if messages:
293
- last_message = messages[-1]
294
- if isinstance(last_message['content'], list):
295
- for content_part in last_message['content']:
296
- if isinstance(content_part, dict) and content_part.get('type') == 'text':
297
- content_part['role'] = 'user'
298
- else:
299
- last_message['id'] = random_id
300
- last_message['role'] = 'user'
301
 
302
  if image is not None:
303
- # Process image if required
304
- # This implementation assumes that image URLs are handled by the external service
305
- pass # Implement as needed
306
-
 
 
 
 
 
 
 
 
 
 
307
  data = {
308
  "messages": messages,
309
  "id": random_id,
@@ -314,7 +108,7 @@ class Blackbox:
314
  "trendingAgentMode": {},
315
  "isMicMode": False,
316
  "userSystemPrompt": None,
317
- "maxTokens": int(os.getenv("MAX_TOKENS", "4096")),
318
  "playgroundTopP": 0.9,
319
  "playgroundTemperature": 0.5,
320
  "isChromeExt": False,
@@ -351,85 +145,77 @@ class Blackbox:
351
  if url_match:
352
  image_url = url_match.group(0)
353
  logger.info(f"Image URL found: {image_url}")
354
- yield ImageResponse(image_url, alt=messages[-1]['content'])
 
 
 
 
 
 
 
 
355
  else:
356
  logger.error("Image URL not found in the response.")
357
  raise Exception("Image URL not found in the response")
358
  else:
359
- async for chunk, info in response.content.iter_chunks():
360
- if chunk:
361
- decoded_chunk = chunk.decode(errors='ignore')
362
- decoded_chunk = re.sub(r'\$@\$v=[^$]+\$@\$', '', decoded_chunk)
363
- if decoded_chunk.strip():
364
- yield decoded_chunk
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  break # Exit the retry loop if successful
366
  except ClientError as ce:
367
  logger.error(f"Client error occurred: {ce}. Retrying attempt {attempt + 1}/{retry_attempts}")
368
  if attempt == retry_attempts - 1:
369
- raise HTTPException(status_code=502, detail="Error communicating with the external API.")
370
  except asyncio.TimeoutError:
371
  logger.error(f"Request timed out. Retrying attempt {attempt + 1}/{retry_attempts}")
372
  if attempt == retry_attempts - 1:
373
- raise HTTPException(status_code=504, detail="External API request timed out.")
374
  except Exception as e:
375
  logger.error(f"Unexpected error: {e}. Retrying attempt {attempt + 1}/{retry_attempts}")
376
  if attempt == retry_attempts - 1:
377
  raise HTTPException(status_code=500, detail=str(e))
378
 
379
- # Pydantic Models
380
- class TextContent(BaseModel):
381
- type: Literal["text"] = Field(..., description="Type of content, e.g., 'text'.")
382
- text: str = Field(..., description="The text content.")
383
-
384
- class ImageURLContent(BaseModel):
385
- type: Literal["image_url"] = Field(..., description="Type of content, e.g., 'image_url'.")
386
- image_url: Dict[str, str] = Field(..., description="Dictionary containing the image URL.")
387
-
388
- Content = Union[TextContent, ImageURLContent]
389
 
390
  class Message(BaseModel):
391
- role: str = Field(..., description="The role of the message author.")
392
- content: Union[str, List[Content]] = Field(..., description="The content of the message. Can be a string or a list of content parts.")
393
-
394
- @validator('content', pre=True)
395
- def validate_content(cls, v):
396
- if isinstance(v, list):
397
- processed_content = []
398
- for item in v:
399
- if 'type' not in item:
400
- raise ValueError("Each content part must have a 'type' field.")
401
- if item['type'] == 'text':
402
- processed_content.append(TextContent(**item))
403
- elif item['type'] == 'image_url':
404
- processed_content.append(ImageURLContent(**item))
405
- else:
406
- raise ValueError(f"Unsupported content type: {item['type']}")
407
- return processed_content
408
- elif isinstance(v, str):
409
- return v
410
- else:
411
- raise ValueError("Content must be either a string or a list of content parts.")
412
 
413
  class ChatRequest(BaseModel):
414
- model: str = Field(..., description="ID of the model to use.")
415
- messages: List[Message] = Field(..., description="A list of messages comprising the conversation.")
416
- stream: Optional[bool] = Field(False, description="Whether to stream the response.")
417
- webSearchMode: Optional[bool] = Field(False, description="Whether to enable web search mode.")
418
-
419
- class ChatCompletionChoice(BaseModel):
420
- index: int
421
- delta: Dict[str, Any]
422
- finish_reason: Optional[str] = None
423
-
424
- class ChatCompletionResponse(BaseModel):
425
- id: str
426
- object: str
427
- created: int
428
  model: str
429
- choices: List[ChatCompletionChoice]
430
- usage: Optional[Dict[str, int]] = None
 
 
431
 
432
- # Utility Function to Create Response
433
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
434
  return {
435
  "id": f"chatcmpl-{uuid.uuid4()}",
@@ -443,58 +229,36 @@ def create_response(content: str, model: str, finish_reason: Optional[str] = Non
443
  "finish_reason": finish_reason,
444
  }
445
  ],
446
- "usage": None, # To be populated if usage metrics are available
447
  }
448
 
449
- # Endpoint: Chat Completions
450
- @app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
451
- @limiter.limit("60/minute") # Example: 60 requests per minute per IP
452
- async def chat_completions(
453
- chat_request: ChatRequest, # Renamed from 'request' to 'chat_request'
454
- request: Request, # Added 'request: Request' parameter
455
- api_key: str = Depends(get_api_key)
456
- ):
457
- logger.info(f"Received chat completions request: {chat_request}")
458
  try:
459
- # Process messages for token counting and sending to Blackbox
460
- processed_messages = []
461
- for msg in chat_request.messages:
462
- if isinstance(msg.content, list):
463
- # Convert list of content parts to a structured format
464
- combined_content = []
465
- for part in msg.content:
466
- if isinstance(part, TextContent):
467
- combined_content.append({"type": part.type, "text": part.text})
468
- elif isinstance(part, ImageURLContent):
469
- combined_content.append({"type": part.type, "image_url": part.image_url})
470
- processed_messages.append({"role": msg.role, "content": combined_content})
471
- else:
472
- processed_messages.append({"role": msg.role, "content": msg.content})
473
-
474
- prompt_tokens = count_tokens(processed_messages, chat_request.model)
475
-
476
  async_generator = Blackbox.create_async_generator(
477
- model=chat_request.model,
478
- messages=processed_messages,
479
- image=None, # Adjust if image handling is required
 
480
  image_name=None,
481
- webSearchMode=chat_request.webSearchMode
482
  )
483
 
484
- if chat_request.stream:
485
  async def generate():
486
  try:
487
- completion_tokens = 0
488
  async for chunk in async_generator:
489
  if isinstance(chunk, ImageResponse):
490
- image_markdown = f"![image]({chunk.url})"
491
- response_chunk = create_response(image_markdown, chat_request.model)
492
- yield f"data: {json.dumps(response_chunk)}\n\n"
493
- completion_tokens += len(image_markdown.split())
494
  else:
495
- response_chunk = create_response(chunk, chat_request.model)
496
- yield f"data: {json.dumps(response_chunk)}\n\n"
497
- completion_tokens += len(chunk.split())
 
498
 
499
  # Signal the end of the stream
500
  yield "data: [DONE]\n\n"
@@ -509,36 +273,34 @@ async def chat_completions(
509
  return StreamingResponse(generate(), media_type="text/event-stream")
510
  else:
511
  response_content = ""
512
- completion_tokens = 0
513
  async for chunk in async_generator:
514
  if isinstance(chunk, ImageResponse):
515
- response_content += f"![image]({chunk.url})\n"
516
- completion_tokens += len(f"![image]({chunk.url})\n".split())
517
  else:
518
  response_content += chunk
519
- completion_tokens += len(chunk.split())
520
-
521
- total_tokens = prompt_tokens + completion_tokens
522
 
523
  logger.info("Completed non-streaming response generation.")
524
- return ChatCompletionResponse(
525
- id=f"chatcmpl-{uuid.uuid4()}",
526
- object="chat.completion",
527
- created=int(datetime.now().timestamp()),
528
- model=chat_request.model,
529
- choices=[
530
- ChatCompletionChoice(
531
- index=0,
532
- delta={"content": response_content, "role": "assistant"},
533
- finish_reason="stop"
534
- )
 
 
 
535
  ],
536
- usage={
537
- "prompt_tokens": prompt_tokens,
538
- "completion_tokens": completion_tokens,
539
- "total_tokens": total_tokens
540
- }
541
- )
542
  except ModelNotWorkingException as e:
543
  logger.warning(f"Model not working: {e}")
544
  raise HTTPException(status_code=503, detail=str(e))
@@ -549,24 +311,19 @@ async def chat_completions(
549
  logger.exception("An unexpected error occurred while processing the chat completions request.")
550
  raise HTTPException(status_code=500, detail=str(e))
551
 
552
- # Endpoint: List Models
553
- @app.get("/v1/models", response_model=Dict[str, List[Dict[str, str]]])
554
- @limiter.limit("60/minute")
555
- async def get_models(
556
- request: Request, # Ensure 'request: Request' parameter is present
557
- api_key: str = Depends(get_api_key)
558
- ):
559
  logger.info("Fetching available models.")
560
  return {"data": [{"id": model} for model in Blackbox.models]}
561
 
562
- # Endpoint: Model Status
563
- @app.get("/v1/models/{model}/status", response_model=Dict[str, str])
564
- @limiter.limit("60/minute")
565
- async def model_status(
566
- model: str,
567
- request: Request, # Ensure 'request: Request' parameter is present
568
- api_key: str = Depends(get_api_key)
569
- ):
570
  """Check if a specific model is available."""
571
  if model in Blackbox.models:
572
  return {"model": model, "status": "available"}
@@ -576,16 +333,6 @@ async def model_status(
576
  else:
577
  raise HTTPException(status_code=404, detail="Model not found")
578
 
579
- # Endpoint: Health Check
580
- @app.get("/v1/health", response_model=Dict[str, str])
581
- @limiter.limit("60/minute")
582
- async def health_check(
583
- request: Request # Ensure 'request: Request' parameter is present
584
- ):
585
- """Health check endpoint to verify the service is running."""
586
- return {"status": "ok"}
587
-
588
- # Run the application
589
  if __name__ == "__main__":
590
  import uvicorn
591
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
1
  import re
2
  import random
3
  import string
 
5
  import json
6
  import logging
7
  import asyncio
8
+ import base64
9
  from aiohttp import ClientSession, ClientTimeout, ClientError
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from pydantic import BaseModel
12
+ from typing import List, Dict, Any, Optional, AsyncGenerator
 
 
13
  from datetime import datetime
14
+ from fastapi.responses import StreamingResponse
 
 
 
 
 
 
 
15
 
16
  # Configure logging
17
  logging.basicConfig(
 
23
  )
24
  logger = logging.getLogger(__name__)
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  # Custom exception for model not working
27
  class ModelNotWorkingException(Exception):
28
  def __init__(self, model: str):
 
30
  self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
31
  super().__init__(self.message)
32
 
33
+ # Proper implementation for ImageResponse and to_data_uri
34
  class ImageResponse:
35
+ def __init__(self, data_uri: str, alt: str):
36
+ self.data_uri = data_uri
37
  self.alt = alt
38
 
39
+ def to_data_uri(image: bytes, mime_type: str = "image/png") -> str:
40
+ encoded = base64.b64encode(image).decode('utf-8')
41
+ return f"data:{mime_type};base64,{encoded}"
42
 
43
+ def decode_base64_image(data_uri: str) -> bytes:
 
 
 
 
 
44
  try:
45
+ header, encoded = data_uri.split(",", 1)
46
+ return base64.b64decode(encoded)
47
+ except Exception as e:
48
+ logger.error(f"Error decoding base64 image: {e}")
49
+ raise e
 
 
 
 
 
 
 
 
 
 
50
 
 
51
  class Blackbox:
52
+ # ... [existing Blackbox class definition]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  @classmethod
55
  async def create_async_generator(
56
  cls,
57
  model: str,
58
+ messages: List[Dict[str, str]],
59
  proxy: Optional[str] = None,
60
+ image: Optional[str] = None, # Expecting a base64 string
61
  image_name: Optional[str] = None,
62
  webSearchMode: bool = False,
63
  **kwargs
 
70
  raise ModelNotWorkingException(model)
71
 
72
  headers = {
73
+ # ... [existing headers]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
 
76
  if model in cls.model_prefixes:
77
  prefix = cls.model_prefixes[model]
78
+ if not messages[0]['content'].startswith(prefix):
79
+ logger.debug(f"Adding prefix '{prefix}' to the first message.")
 
 
 
 
 
 
80
  messages[0]['content'] = f"{prefix} {messages[0]['content']}"
81
+
82
  random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
83
+ messages[-1]['id'] = random_id
84
+ messages[-1]['role'] = 'user'
 
 
 
 
 
 
 
 
85
 
86
  if image is not None:
87
+ try:
88
+ image_bytes = decode_base64_image(image)
89
+ data_uri = to_data_uri(image_bytes)
90
+ messages[-1]['data'] = {
91
+ 'fileText': '',
92
+ 'imageBase64': data_uri,
93
+ 'title': image_name
94
+ }
95
+ messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
96
+ logger.debug("Image data added to the message.")
97
+ except Exception as e:
98
+ logger.error(f"Failed to decode base64 image: {e}")
99
+ raise HTTPException(status_code=400, detail="Invalid image data provided.")
100
+
101
  data = {
102
  "messages": messages,
103
  "id": random_id,
 
108
  "trendingAgentMode": {},
109
  "isMicMode": False,
110
  "userSystemPrompt": None,
111
+ "maxTokens": 99999999,
112
  "playgroundTopP": 0.9,
113
  "playgroundTemperature": 0.5,
114
  "isChromeExt": False,
 
145
  if url_match:
146
  image_url = url_match.group(0)
147
  logger.info(f"Image URL found: {image_url}")
148
+
149
+ # Fetch the image data
150
+ async with session.get(image_url) as img_response:
151
+ img_response.raise_for_status()
152
+ image_bytes = await img_response.read()
153
+ data_uri = to_data_uri(image_bytes)
154
+ logger.info("Image converted to base64 data URI.")
155
+
156
+ yield ImageResponse(data_uri, alt=messages[-1]['content'])
157
  else:
158
  logger.error("Image URL not found in the response.")
159
  raise Exception("Image URL not found in the response")
160
  else:
161
+ full_response = ""
162
+ search_results_json = ""
163
+ try:
164
+ async for chunk, _ in response.content.iter_chunks():
165
+ if chunk:
166
+ decoded_chunk = chunk.decode(errors='ignore')
167
+ decoded_chunk = re.sub(r'\$@\$v=[^$]+\$@\$', '', decoded_chunk)
168
+ if decoded_chunk.strip():
169
+ if '$~~~$' in decoded_chunk:
170
+ search_results_json += decoded_chunk
171
+ else:
172
+ full_response += decoded_chunk
173
+ yield decoded_chunk
174
+ logger.info("Finished streaming response chunks.")
175
+ except Exception as e:
176
+ logger.exception("Error while iterating over response chunks.")
177
+ raise e
178
+ if data["webSearchMode"] and search_results_json:
179
+ match = re.search(r'\$~~~\$(.*?)\$~~~\$', search_results_json, re.DOTALL)
180
+ if match:
181
+ try:
182
+ search_results = json.loads(match.group(1))
183
+ formatted_results = "\n\n**Sources:**\n"
184
+ for i, result in enumerate(search_results[:5], 1):
185
+ formatted_results += f"{i}. [{result['title']}]({result['link']})\n"
186
+ logger.info("Formatted search results.")
187
+ yield formatted_results
188
+ except json.JSONDecodeError as je:
189
+ logger.error("Failed to parse search results JSON.")
190
+ raise je
191
  break # Exit the retry loop if successful
192
  except ClientError as ce:
193
  logger.error(f"Client error occurred: {ce}. Retrying attempt {attempt + 1}/{retry_attempts}")
194
  if attempt == retry_attempts - 1:
195
+ raise HTTPException(status_code=502, detail="Error communicating with the external API. | NiansuhAI")
196
  except asyncio.TimeoutError:
197
  logger.error(f"Request timed out. Retrying attempt {attempt + 1}/{retry_attempts}")
198
  if attempt == retry_attempts - 1:
199
+ raise HTTPException(status_code=504, detail="External API request timed out. | NiansuhAI")
200
  except Exception as e:
201
  logger.error(f"Unexpected error: {e}. Retrying attempt {attempt + 1}/{retry_attempts}")
202
  if attempt == retry_attempts - 1:
203
  raise HTTPException(status_code=500, detail=str(e))
204
 
205
+ # FastAPI app setup
206
+ app = FastAPI()
 
 
 
 
 
 
 
 
207
 
208
  class Message(BaseModel):
209
+ role: str
210
+ content: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  class ChatRequest(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  model: str
214
+ messages: List[Message]
215
+ stream: Optional[bool] = False
216
+ webSearchMode: Optional[bool] = False
217
+ image: Optional[str] = None # Add image field for base64 data
218
 
 
219
  def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
220
  return {
221
  "id": f"chatcmpl-{uuid.uuid4()}",
 
229
  "finish_reason": finish_reason,
230
  }
231
  ],
232
+ "usage": None,
233
  }
234
 
235
+ @app.post("/niansuhai/v1/chat/completions")
236
+ async def chat_completions(request: ChatRequest, req: Request):
237
+ logger.info(f"Received chat completions request: {request}")
 
 
 
 
 
 
238
  try:
239
+ messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
240
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  async_generator = Blackbox.create_async_generator(
242
+ model=request.model,
243
+ messages=messages,
244
+ proxy=None, # Pass proxy if needed
245
+ image=request.image, # Pass the base64 image
246
  image_name=None,
247
+ webSearchMode=request.webSearchMode
248
  )
249
 
250
+ if request.stream:
251
  async def generate():
252
  try:
 
253
  async for chunk in async_generator:
254
  if isinstance(chunk, ImageResponse):
255
+ image_markdown = f"![{chunk.alt}]({chunk.data_uri})"
256
+ response_chunk = create_response(image_markdown, request.model)
 
 
257
  else:
258
+ response_chunk = create_response(chunk, request.model)
259
+
260
+ # Yield each chunk in SSE format
261
+ yield f"data: {json.dumps(response_chunk)}\n\n"
262
 
263
  # Signal the end of the stream
264
  yield "data: [DONE]\n\n"
 
273
  return StreamingResponse(generate(), media_type="text/event-stream")
274
  else:
275
  response_content = ""
 
276
  async for chunk in async_generator:
277
  if isinstance(chunk, ImageResponse):
278
+ response_content += f"![{chunk.alt}]({chunk.data_uri})\n"
 
279
  else:
280
  response_content += chunk
 
 
 
281
 
282
  logger.info("Completed non-streaming response generation.")
283
+ return {
284
+ "id": f"chatcmpl-{uuid.uuid4()}",
285
+ "object": "chat.completion",
286
+ "created": int(datetime.now().timestamp()),
287
+ "model": request.model,
288
+ "choices": [
289
+ {
290
+ "message": {
291
+ "role": "assistant",
292
+ "content": response_content
293
+ },
294
+ "finish_reason": "stop",
295
+ "index": 0
296
+ }
297
  ],
298
+ "usage": {
299
+ "prompt_tokens": sum(len(msg['content'].split()) for msg in messages),
300
+ "completion_tokens": len(response_content.split()),
301
+ "total_tokens": sum(len(msg['content'].split()) for msg in messages) + len(response_content.split())
302
+ },
303
+ }
304
  except ModelNotWorkingException as e:
305
  logger.warning(f"Model not working: {e}")
306
  raise HTTPException(status_code=503, detail=str(e))
 
311
  logger.exception("An unexpected error occurred while processing the chat completions request.")
312
  raise HTTPException(status_code=500, detail=str(e))
313
 
314
+ @app.get("/niansuhai/v1/models")
315
+ async def get_models():
 
 
 
 
 
316
  logger.info("Fetching available models.")
317
  return {"data": [{"id": model} for model in Blackbox.models]}
318
 
319
+ # Additional endpoints for better functionality
320
+ @app.get("/niansuhai/v1/health")
321
+ async def health_check():
322
+ """Health check endpoint to verify the service is running."""
323
+ return {"status": "ok"}
324
+
325
+ @app.get("/niansuhai/v1/models/{model}/status")
326
+ async def model_status(model: str):
327
  """Check if a specific model is available."""
328
  if model in Blackbox.models:
329
  return {"model": model, "status": "available"}
 
333
  else:
334
  raise HTTPException(status_code=404, detail="Model not found")
335
 
 
 
 
 
 
 
 
 
 
 
336
  if __name__ == "__main__":
337
  import uvicorn
338
  uvicorn.run(app, host="0.0.0.0", port=8000)