Update main.py
Browse files
main.py
CHANGED
@@ -10,7 +10,7 @@ import logging
|
|
10 |
import asyncio
|
11 |
import time
|
12 |
from collections import defaultdict
|
13 |
-
from typing import List, Dict, Any, Optional, AsyncGenerator, Union
|
14 |
|
15 |
from datetime import datetime
|
16 |
|
@@ -145,10 +145,10 @@ class ImageResponseCustom:
|
|
145 |
self.url = url
|
146 |
self.alt = alt
|
147 |
|
148 |
-
# Blackbox AI Integration
|
149 |
class Blackbox:
|
150 |
url = "https://www.blackbox.ai"
|
151 |
-
api_endpoint = "https://www.blackbox.ai/api/chat"
|
152 |
working = True
|
153 |
supports_stream = True
|
154 |
supports_system_message = True
|
@@ -266,7 +266,7 @@ class Blackbox:
|
|
266 |
async def create_async_generator(
|
267 |
cls,
|
268 |
model: str,
|
269 |
-
messages: List[Dict[str,
|
270 |
proxy: Optional[str] = None,
|
271 |
image: Optional[str] = None,
|
272 |
image_name: Optional[str] = None,
|
@@ -283,7 +283,7 @@ class Blackbox:
|
|
283 |
if not cls.working or model not in cls.models:
|
284 |
logger.error(f"Model {model} is not working or not supported.")
|
285 |
raise ModelNotWorkingException(model)
|
286 |
-
|
287 |
headers = {
|
288 |
"accept": "*/*",
|
289 |
"accept-language": "en-US,en;q=0.9",
|
@@ -307,7 +307,7 @@ class Blackbox:
|
|
307 |
if not messages[0]['content'].startswith(prefix):
|
308 |
logger.debug(f"Adding prefix '{prefix}' to the first message.")
|
309 |
messages[0]['content'] = f"{prefix} {messages[0]['content']}"
|
310 |
-
|
311 |
random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
|
312 |
messages[-1]['id'] = random_id
|
313 |
messages[-1]['role'] = 'user'
|
@@ -323,7 +323,7 @@ class Blackbox:
|
|
323 |
}
|
324 |
messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
|
325 |
logger.debug("Image data added to the message.")
|
326 |
-
|
327 |
data = {
|
328 |
"messages": messages,
|
329 |
"id": random_id,
|
@@ -454,14 +454,36 @@ async def security_middleware(request: Request, call_next):
|
|
454 |
return response
|
455 |
|
456 |
# Request Models
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
457 |
class Message(BaseModel):
|
458 |
role: str
|
459 |
-
content:
|
460 |
|
461 |
-
@validator('
|
462 |
-
def
|
463 |
-
if not
|
464 |
-
raise ValueError(
|
465 |
return v
|
466 |
|
467 |
class ChatRequest(BaseModel):
|
@@ -478,7 +500,6 @@ class ChatRequest(BaseModel):
|
|
478 |
logit_bias: Optional[Dict[str, float]] = None
|
479 |
user: Optional[str] = None
|
480 |
webSearchMode: Optional[bool] = False # Custom parameter
|
481 |
-
image: Optional[str] = None # Base64-encoded image
|
482 |
|
483 |
class TokenizerRequest(BaseModel):
|
484 |
text: str
|
@@ -511,6 +532,41 @@ def create_response(content: str, model: str, finish_reason: Optional[str] = Non
|
|
511 |
"usage": None, # To be filled in non-streaming responses
|
512 |
}
|
513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
514 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
|
515 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
516 |
client_ip = req.client.host
|
@@ -525,134 +581,52 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
|
|
525 |
logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
|
526 |
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
527 |
|
528 |
-
#
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
#
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
}
|
575 |
-
],
|
576 |
-
"usage": None, # Usage can be updated if you track tokens in real-time
|
577 |
-
}
|
578 |
-
yield f"data: {json.dumps(response_chunk)}\n\n"
|
579 |
-
|
580 |
-
# After all chunks are sent, send the final message with finish_reason
|
581 |
-
prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
|
582 |
-
completion_tokens = len(assistant_content.split())
|
583 |
-
total_tokens = prompt_tokens + completion_tokens
|
584 |
-
estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
|
585 |
-
|
586 |
-
final_response = {
|
587 |
-
"id": f"chatcmpl-{uuid.uuid4()}",
|
588 |
-
"object": "chat.completion",
|
589 |
-
"created": int(datetime.now().timestamp()),
|
590 |
-
"model": request.model,
|
591 |
-
"choices": [
|
592 |
-
{
|
593 |
-
"message": {
|
594 |
-
"role": "assistant",
|
595 |
-
"content": assistant_content
|
596 |
-
},
|
597 |
-
"finish_reason": "stop",
|
598 |
-
"index": 0
|
599 |
-
}
|
600 |
-
],
|
601 |
-
"usage": {
|
602 |
-
"prompt_tokens": prompt_tokens,
|
603 |
-
"completion_tokens": completion_tokens,
|
604 |
-
"total_tokens": total_tokens,
|
605 |
-
"estimated_cost": estimated_cost
|
606 |
-
},
|
607 |
-
}
|
608 |
-
yield f"data: {json.dumps(final_response)}\n\n"
|
609 |
-
yield "data: [DONE]\n\n"
|
610 |
-
except HTTPException as he:
|
611 |
-
error_response = {"error": he.detail}
|
612 |
-
yield f"data: {json.dumps(error_response)}\n\n"
|
613 |
-
except Exception as e:
|
614 |
-
logger.exception(f"Error during streaming response generation from IP: {client_ip}.")
|
615 |
-
error_response = {"error": str(e)}
|
616 |
-
yield f"data: {json.dumps(error_response)}\n\n"
|
617 |
-
|
618 |
-
return StreamingResponse(generate(), media_type="text/event-stream")
|
619 |
-
else:
|
620 |
-
response_content = ""
|
621 |
-
async for chunk in async_generator:
|
622 |
-
if isinstance(chunk, ImageResponseCustom):
|
623 |
-
response_content += f"\n"
|
624 |
-
else:
|
625 |
-
response_content += chunk
|
626 |
-
|
627 |
-
prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
|
628 |
-
completion_tokens = len(response_content.split())
|
629 |
-
total_tokens = prompt_tokens + completion_tokens
|
630 |
-
estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
|
631 |
-
|
632 |
-
logger.info(f"Completed non-streaming response generation for API key: {api_key} | IP: {client_ip}")
|
633 |
-
|
634 |
-
return {
|
635 |
-
"id": f"chatcmpl-{uuid.uuid4()}",
|
636 |
-
"object": "chat.completion",
|
637 |
-
"created": int(datetime.now().timestamp()),
|
638 |
-
"model": request.model,
|
639 |
-
"choices": [
|
640 |
-
{
|
641 |
-
"message": {
|
642 |
-
"role": "assistant",
|
643 |
-
"content": response_content
|
644 |
-
},
|
645 |
-
"finish_reason": "stop",
|
646 |
-
"index": 0
|
647 |
-
}
|
648 |
-
],
|
649 |
-
"usage": {
|
650 |
-
"prompt_tokens": prompt_tokens,
|
651 |
-
"completion_tokens": completion_tokens,
|
652 |
-
"total_tokens": total_tokens,
|
653 |
-
"estimated_cost": estimated_cost
|
654 |
-
},
|
655 |
-
}
|
656 |
except ModelNotWorkingException as e:
|
657 |
logger.warning(f"Model not working: {e} | IP: {client_ip}")
|
658 |
raise HTTPException(status_code=503, detail=str(e))
|
|
|
10 |
import asyncio
|
11 |
import time
|
12 |
from collections import defaultdict
|
13 |
+
from typing import List, Dict, Any, Optional, AsyncGenerator, Union, Tuple
|
14 |
|
15 |
from datetime import datetime
|
16 |
|
|
|
145 |
self.url = url
|
146 |
self.alt = alt
|
147 |
|
148 |
+
# Blackbox AI Integration (Placeholder for actual implementation)
|
149 |
class Blackbox:
|
150 |
url = "https://www.blackbox.ai"
|
151 |
+
api_endpoint = "https://www.blackbox.ai/api/chat" # Placeholder endpoint
|
152 |
working = True
|
153 |
supports_stream = True
|
154 |
supports_system_message = True
|
|
|
266 |
async def create_async_generator(
|
267 |
cls,
|
268 |
model: str,
|
269 |
+
messages: List[Dict[str, Any]],
|
270 |
proxy: Optional[str] = None,
|
271 |
image: Optional[str] = None,
|
272 |
image_name: Optional[str] = None,
|
|
|
283 |
if not cls.working or model not in cls.models:
|
284 |
logger.error(f"Model {model} is not working or not supported.")
|
285 |
raise ModelNotWorkingException(model)
|
286 |
+
|
287 |
headers = {
|
288 |
"accept": "*/*",
|
289 |
"accept-language": "en-US,en;q=0.9",
|
|
|
307 |
if not messages[0]['content'].startswith(prefix):
|
308 |
logger.debug(f"Adding prefix '{prefix}' to the first message.")
|
309 |
messages[0]['content'] = f"{prefix} {messages[0]['content']}"
|
310 |
+
|
311 |
random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
|
312 |
messages[-1]['id'] = random_id
|
313 |
messages[-1]['role'] = 'user'
|
|
|
323 |
}
|
324 |
messages[-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + messages[-1]['content']
|
325 |
logger.debug("Image data added to the message.")
|
326 |
+
|
327 |
data = {
|
328 |
"messages": messages,
|
329 |
"id": random_id,
|
|
|
454 |
return response
|
455 |
|
456 |
# Request Models
|
457 |
+
class TextContent(BaseModel):
|
458 |
+
type: str = "text"
|
459 |
+
text: str
|
460 |
+
|
461 |
+
@validator('type')
|
462 |
+
def type_must_be_text(cls, v):
|
463 |
+
if v != "text":
|
464 |
+
raise ValueError("Type must be 'text'")
|
465 |
+
return v
|
466 |
+
|
467 |
+
class ImageContent(BaseModel):
|
468 |
+
type: str = "image_url"
|
469 |
+
image_url: Dict[str, str]
|
470 |
+
|
471 |
+
@validator('type')
|
472 |
+
def type_must_be_image_url(cls, v):
|
473 |
+
if v != "image_url":
|
474 |
+
raise ValueError("Type must be 'image_url'")
|
475 |
+
return v
|
476 |
+
|
477 |
+
ContentItem = Union[TextContent, ImageContent]
|
478 |
+
|
479 |
class Message(BaseModel):
|
480 |
role: str
|
481 |
+
content: List[ContentItem]
|
482 |
|
483 |
+
@validator('role')
|
484 |
+
def role_must_be_valid(cls, v):
|
485 |
+
if v not in {"system", "user", "assistant"}:
|
486 |
+
raise ValueError("Role must be 'system', 'user', or 'assistant'")
|
487 |
return v
|
488 |
|
489 |
class ChatRequest(BaseModel):
|
|
|
500 |
logit_bias: Optional[Dict[str, float]] = None
|
501 |
user: Optional[str] = None
|
502 |
webSearchMode: Optional[bool] = False # Custom parameter
|
|
|
503 |
|
504 |
class TokenizerRequest(BaseModel):
|
505 |
text: str
|
|
|
532 |
"usage": None, # To be filled in non-streaming responses
|
533 |
}
|
534 |
|
535 |
+
def extract_image_from_content(content: str) -> Optional[Tuple[str, str]]:
|
536 |
+
"""
|
537 |
+
Extracts the first image from the content string.
|
538 |
+
Returns a tuple of (alt_text, image_data_uri) if found, else None.
|
539 |
+
"""
|
540 |
+
# Regex to match markdown image syntax: 
|
541 |
+
match = re.search(r'!\[([^\]]*)\]\((data:image/\w+;base64,[^\)]+)\)', content)
|
542 |
+
if match:
|
543 |
+
alt_text = match.group(1)
|
544 |
+
image_data_uri = match.group(2)
|
545 |
+
return alt_text, image_data_uri
|
546 |
+
return None
|
547 |
+
|
548 |
+
def extract_all_images_from_content(content: str) -> List[Tuple[str, str]]:
|
549 |
+
"""
|
550 |
+
Extracts all images from the content string.
|
551 |
+
Returns a list of tuples containing (alt_text, image_data_uri).
|
552 |
+
"""
|
553 |
+
# Regex to match markdown image syntax: 
|
554 |
+
matches = re.findall(r'!\[([^\]]*)\]\((data:image/\w+;base64,[^\)]+)\)', content)
|
555 |
+
return matches if matches else []
|
556 |
+
|
557 |
+
async def analyze_image(image_data_uri: str) -> str:
|
558 |
+
"""
|
559 |
+
Placeholder function to analyze the image.
|
560 |
+
Replace this with actual image analysis logic or API calls.
|
561 |
+
"""
|
562 |
+
# Extract base64 data
|
563 |
+
image_data = image_data_uri.split(",")[1]
|
564 |
+
# Decode and process the image as needed
|
565 |
+
# For example, send it to an external API
|
566 |
+
# Here, we'll return a dummy response
|
567 |
+
await asyncio.sleep(1) # Simulate processing delay
|
568 |
+
return "Image analysis result: The image depicts a beautiful sunset over the mountains."
|
569 |
+
|
570 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
|
571 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
572 |
client_ip = req.client.host
|
|
|
581 |
logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
|
582 |
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
583 |
|
584 |
+
# Initialize response content
|
585 |
+
assistant_content = ""
|
586 |
+
|
587 |
+
# Iterate through messages to find and process images
|
588 |
+
for msg in request.messages:
|
589 |
+
if msg.role == "user":
|
590 |
+
# Extract all images from the message content
|
591 |
+
images = extract_all_images_from_content(" ".join([item.text if item.type == "text" else item.image_url['url'] for item in msg.content]))
|
592 |
+
for alt_text, image_data_uri in images:
|
593 |
+
# Analyze the image
|
594 |
+
analysis_result = await analyze_image(image_data_uri)
|
595 |
+
assistant_content += analysis_result + "\n"
|
596 |
+
|
597 |
+
# Example response content
|
598 |
+
assistant_content += "Based on the image you provided, here are the insights..."
|
599 |
+
|
600 |
+
# Calculate token usage (simple approximation)
|
601 |
+
prompt_tokens = sum(len(" ".join([item.text if item.type == "text" else item.image_url['url'] for item in msg.content]).split()) for msg in request.messages)
|
602 |
+
completion_tokens = len(assistant_content.split())
|
603 |
+
total_tokens = prompt_tokens + completion_tokens
|
604 |
+
estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
|
605 |
+
|
606 |
+
logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
|
607 |
+
|
608 |
+
return {
|
609 |
+
"id": f"chatcmpl-{uuid.uuid4()}",
|
610 |
+
"object": "chat.completion",
|
611 |
+
"created": int(datetime.now().timestamp()),
|
612 |
+
"model": request.model,
|
613 |
+
"choices": [
|
614 |
+
{
|
615 |
+
"message": {
|
616 |
+
"role": "assistant",
|
617 |
+
"content": assistant_content.strip()
|
618 |
+
},
|
619 |
+
"finish_reason": "stop",
|
620 |
+
"index": 0
|
621 |
+
}
|
622 |
+
],
|
623 |
+
"usage": {
|
624 |
+
"prompt_tokens": prompt_tokens,
|
625 |
+
"completion_tokens": completion_tokens,
|
626 |
+
"total_tokens": total_tokens,
|
627 |
+
"estimated_cost": estimated_cost
|
628 |
+
},
|
629 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
630 |
except ModelNotWorkingException as e:
|
631 |
logger.warning(f"Model not working: {e} | IP: {client_ip}")
|
632 |
raise HTTPException(status_code=503, detail=str(e))
|