Update main.py
Browse files
main.py
CHANGED
@@ -19,7 +19,7 @@ from pydantic import BaseModel
|
|
19 |
# Configure logging
|
20 |
logging.basicConfig(
|
21 |
level=logging.INFO,
|
22 |
-
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
23 |
handlers=[logging.StreamHandler()]
|
24 |
)
|
25 |
logger = logging.getLogger(__name__)
|
@@ -373,13 +373,19 @@ class Blackbox:
|
|
373 |
# FastAPI app setup
|
374 |
app = FastAPI()
|
375 |
|
376 |
-
# Middleware to enhance security
|
377 |
@app.middleware("http")
|
378 |
async def security_middleware(request: Request, call_next):
|
|
|
|
|
|
|
|
|
|
|
379 |
# Enforce that POST requests to sensitive endpoints must have a valid Content-Type
|
380 |
-
if request.method == "POST" and request.url.path
|
381 |
content_type = request.headers.get("Content-Type")
|
382 |
if content_type != "application/json":
|
|
|
383 |
return JSONResponse(
|
384 |
status_code=400,
|
385 |
content={
|
@@ -391,7 +397,11 @@ async def security_middleware(request: Request, call_next):
|
|
391 |
}
|
392 |
},
|
393 |
)
|
|
|
|
|
394 |
response = await call_next(request)
|
|
|
|
|
395 |
return response
|
396 |
|
397 |
class Message(BaseModel):
|
@@ -431,15 +441,16 @@ def create_response(content: str, model: str, finish_reason: Optional[str] = Non
|
|
431 |
|
432 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter)])
|
433 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
|
|
434 |
# Redact user messages only for logging purposes
|
435 |
redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
|
436 |
|
437 |
-
logger.info(f"Received chat completions request from API key: {api_key} | Model: {request.model} | Messages: {redacted_messages}")
|
438 |
|
439 |
try:
|
440 |
# Validate that the requested model is available
|
441 |
if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
|
442 |
-
logger.warning(f"Attempt to use unavailable model: {request.model}")
|
443 |
raise HTTPException(status_code=400, detail="Requested model is not available. | NiansuhAI")
|
444 |
|
445 |
# Process the request with actual message content, but don't log it
|
@@ -481,7 +492,7 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
|
|
481 |
else:
|
482 |
response_content += chunk
|
483 |
|
484 |
-
logger.info(f"Completed non-streaming response generation for API key: {api_key}")
|
485 |
return {
|
486 |
"id": f"chatcmpl-{uuid.uuid4()}",
|
487 |
"object": "chat.completion",
|
@@ -504,15 +515,17 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
|
|
504 |
},
|
505 |
}
|
506 |
except ModelNotWorkingException as e:
|
507 |
-
logger.warning(f"Model not working: {e}")
|
508 |
raise HTTPException(status_code=503, detail=str(e))
|
509 |
except HTTPException as he:
|
510 |
-
logger.warning(f"HTTPException: {he.detail}")
|
511 |
raise he
|
512 |
except Exception as e:
|
513 |
logger.exception("An unexpected error occurred while processing the chat completions request.")
|
514 |
raise HTTPException(status_code=500, detail=str(e))
|
515 |
|
|
|
|
|
516 |
# Return 'about:blank' when accessing the endpoint via GET
|
517 |
@app.get("/v1/chat/completions")
|
518 |
async def chat_completions_get():
|
@@ -520,31 +533,36 @@ async def chat_completions_get():
|
|
520 |
return RedirectResponse(url='about:blank')
|
521 |
|
522 |
@app.get("/v1/models")
|
523 |
-
async def get_models():
|
524 |
-
|
|
|
525 |
return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
|
526 |
|
527 |
# Additional endpoints for better functionality
|
528 |
@app.get("/v1/health")
|
529 |
async def health_check(req: Request):
|
530 |
-
|
|
|
531 |
return {"status": "ok"}
|
532 |
|
533 |
@app.get("/v1/models/{model}/status")
|
534 |
-
async def model_status(model: str):
|
535 |
-
|
|
|
536 |
if model in Blackbox.models:
|
537 |
return {"model": model, "status": "available"}
|
538 |
elif model in Blackbox.model_aliases and Blackbox.model_aliases[model] in Blackbox.models:
|
539 |
actual_model = Blackbox.model_aliases[model]
|
540 |
return {"model": actual_model, "status": "available via alias"}
|
541 |
else:
|
542 |
-
logger.warning(f"Model not found: {model}")
|
543 |
raise HTTPException(status_code=404, detail="Model not found")
|
544 |
|
545 |
# Custom exception handler to match OpenAI's error format
|
546 |
@app.exception_handler(HTTPException)
|
547 |
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
|
|
|
548 |
return JSONResponse(
|
549 |
status_code=exc.status_code,
|
550 |
content={
|
@@ -562,68 +580,13 @@ class TokenizerRequest(BaseModel):
|
|
562 |
text: str
|
563 |
|
564 |
@app.post("/v1/tokenizer")
|
565 |
-
async def tokenizer(request: TokenizerRequest):
|
|
|
566 |
text = request.text
|
567 |
token_count = len(text.split())
|
|
|
568 |
return {"text": text, "tokens": token_count}
|
569 |
|
570 |
-
# New endpoint: /v1/completions to support text completions
|
571 |
-
class CompletionRequest(BaseModel):
|
572 |
-
model: str
|
573 |
-
prompt: str
|
574 |
-
max_tokens: Optional[int] = 16
|
575 |
-
temperature: Optional[float] = 1.0
|
576 |
-
top_p: Optional[float] = 1.0
|
577 |
-
n: Optional[int] = 1
|
578 |
-
stream: Optional[bool] = False
|
579 |
-
stop: Optional[Union[str, List[str]]] = None
|
580 |
-
logprobs: Optional[int] = None
|
581 |
-
echo: Optional[bool] = False
|
582 |
-
presence_penalty: Optional[float] = 0.0
|
583 |
-
frequency_penalty: Optional[float] = 0.0
|
584 |
-
best_of: Optional[int] = 1
|
585 |
-
logit_bias: Optional[Dict[str, float]] = None
|
586 |
-
user: Optional[str] = None
|
587 |
-
|
588 |
-
@app.post("/v1/completions")
|
589 |
-
async def completions(request: CompletionRequest, req: Request):
|
590 |
-
logger.info(f"Received completion request | Model: {request.model}")
|
591 |
-
|
592 |
-
try:
|
593 |
-
# Validate that the requested model is available
|
594 |
-
if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
|
595 |
-
logger.warning(f"Attempt to use unavailable model: {request.model}")
|
596 |
-
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
597 |
-
|
598 |
-
# Simulate a simple completion by echoing the prompt
|
599 |
-
completion_text = f"{request.prompt} [Completed by {request.model}]"
|
600 |
-
|
601 |
-
return {
|
602 |
-
"id": f"cmpl-{uuid.uuid4()}",
|
603 |
-
"object": "text_completion",
|
604 |
-
"created": int(datetime.now().timestamp()),
|
605 |
-
"model": request.model,
|
606 |
-
"choices": [
|
607 |
-
{
|
608 |
-
"text": completion_text,
|
609 |
-
"index": 0,
|
610 |
-
"logprobs": None,
|
611 |
-
"finish_reason": "length"
|
612 |
-
}
|
613 |
-
],
|
614 |
-
"usage": {
|
615 |
-
"prompt_tokens": len(request.prompt.split()),
|
616 |
-
"completion_tokens": len(completion_text.split()),
|
617 |
-
"total_tokens": len(request.prompt.split()) + len(completion_text.split())
|
618 |
-
}
|
619 |
-
}
|
620 |
-
except HTTPException as he:
|
621 |
-
logger.warning(f"HTTPException: {he.detail}")
|
622 |
-
raise he
|
623 |
-
except Exception as e:
|
624 |
-
logger.exception("An unexpected error occurred while processing the completions request.")
|
625 |
-
raise HTTPException(status_code=500, detail=str(e))
|
626 |
-
|
627 |
if __name__ == "__main__":
|
628 |
import uvicorn
|
629 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
19 |
# Configure logging
|
20 |
logging.basicConfig(
|
21 |
level=logging.INFO,
|
22 |
+
format="%(asctime)s [%(levelname)s] %(name)s [IP: %(client_ip)s]: %(message)s",
|
23 |
handlers=[logging.StreamHandler()]
|
24 |
)
|
25 |
logger = logging.getLogger(__name__)
|
|
|
373 |
# FastAPI app setup
|
374 |
app = FastAPI()
|
375 |
|
376 |
+
# Middleware to enhance security and log client IP
|
377 |
@app.middleware("http")
|
378 |
async def security_middleware(request: Request, call_next):
|
379 |
+
client_ip = request.client.host
|
380 |
+
# Enrich the logger with client_ip
|
381 |
+
extra = {"client_ip": client_ip}
|
382 |
+
logger = logging.LoggerAdapter(logging.getLogger(__name__), extra)
|
383 |
+
|
384 |
# Enforce that POST requests to sensitive endpoints must have a valid Content-Type
|
385 |
+
if request.method == "POST" and request.url.path == "/v1/chat/completions":
|
386 |
content_type = request.headers.get("Content-Type")
|
387 |
if content_type != "application/json":
|
388 |
+
logger.warning("Invalid Content-Type for /v1/chat/completions")
|
389 |
return JSONResponse(
|
390 |
status_code=400,
|
391 |
content={
|
|
|
397 |
}
|
398 |
},
|
399 |
)
|
400 |
+
# Log the incoming request
|
401 |
+
logger.info(f"Incoming request: {request.method} {request.url.path}")
|
402 |
response = await call_next(request)
|
403 |
+
# Log the response status
|
404 |
+
logger.info(f"Response status: {response.status_code}")
|
405 |
return response
|
406 |
|
407 |
class Message(BaseModel):
|
|
|
441 |
|
442 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter)])
|
443 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
444 |
+
client_ip = req.client.host
|
445 |
# Redact user messages only for logging purposes
|
446 |
redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
|
447 |
|
448 |
+
logger.info(f"Received chat completions request from API key: {api_key} | Client IP: {client_ip} | Model: {request.model} | Messages: {redacted_messages}")
|
449 |
|
450 |
try:
|
451 |
# Validate that the requested model is available
|
452 |
if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
|
453 |
+
logger.warning(f"Attempt to use unavailable model: {request.model} | Client IP: {client_ip}")
|
454 |
raise HTTPException(status_code=400, detail="Requested model is not available. | NiansuhAI")
|
455 |
|
456 |
# Process the request with actual message content, but don't log it
|
|
|
492 |
else:
|
493 |
response_content += chunk
|
494 |
|
495 |
+
logger.info(f"Completed non-streaming response generation for API key: {api_key} | Client IP: {client_ip}")
|
496 |
return {
|
497 |
"id": f"chatcmpl-{uuid.uuid4()}",
|
498 |
"object": "chat.completion",
|
|
|
515 |
},
|
516 |
}
|
517 |
except ModelNotWorkingException as e:
|
518 |
+
logger.warning(f"Model not working: {e} | Client IP: {client_ip}")
|
519 |
raise HTTPException(status_code=503, detail=str(e))
|
520 |
except HTTPException as he:
|
521 |
+
logger.warning(f"HTTPException: {he.detail} | Client IP: {client_ip}")
|
522 |
raise he
|
523 |
except Exception as e:
|
524 |
logger.exception("An unexpected error occurred while processing the chat completions request.")
|
525 |
raise HTTPException(status_code=500, detail=str(e))
|
526 |
|
527 |
+
# Removed the /v1/completions endpoint as per user request
|
528 |
+
|
529 |
# Return 'about:blank' when accessing the endpoint via GET
|
530 |
@app.get("/v1/chat/completions")
|
531 |
async def chat_completions_get():
|
|
|
533 |
return RedirectResponse(url='about:blank')
|
534 |
|
535 |
@app.get("/v1/models")
|
536 |
+
async def get_models(req: Request):
|
537 |
+
client_ip = req.client.host
|
538 |
+
logger.info(f"Fetching available models | Client IP: {client_ip}")
|
539 |
return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
|
540 |
|
541 |
# Additional endpoints for better functionality
|
542 |
@app.get("/v1/health")
|
543 |
async def health_check(req: Request):
|
544 |
+
client_ip = req.client.host
|
545 |
+
logger.info(f"Health check requested | Client IP: {client_ip}")
|
546 |
return {"status": "ok"}
|
547 |
|
548 |
@app.get("/v1/models/{model}/status")
|
549 |
+
async def model_status(model: str, req: Request):
|
550 |
+
client_ip = req.client.host
|
551 |
+
logger.info(f"Model status requested for '{model}' | Client IP: {client_ip}")
|
552 |
if model in Blackbox.models:
|
553 |
return {"model": model, "status": "available"}
|
554 |
elif model in Blackbox.model_aliases and Blackbox.model_aliases[model] in Blackbox.models:
|
555 |
actual_model = Blackbox.model_aliases[model]
|
556 |
return {"model": actual_model, "status": "available via alias"}
|
557 |
else:
|
558 |
+
logger.warning(f"Model not found: {model} | Client IP: {client_ip}")
|
559 |
raise HTTPException(status_code=404, detail="Model not found")
|
560 |
|
561 |
# Custom exception handler to match OpenAI's error format
|
562 |
@app.exception_handler(HTTPException)
|
563 |
async def http_exception_handler(request: Request, exc: HTTPException):
|
564 |
+
client_ip = request.client.host
|
565 |
+
logger.error(f"HTTPException: {exc.detail} | Client IP: {client_ip}")
|
566 |
return JSONResponse(
|
567 |
status_code=exc.status_code,
|
568 |
content={
|
|
|
580 |
text: str
|
581 |
|
582 |
@app.post("/v1/tokenizer")
|
583 |
+
async def tokenizer(request: TokenizerRequest, req: Request):
|
584 |
+
client_ip = req.client.host
|
585 |
text = request.text
|
586 |
token_count = len(text.split())
|
587 |
+
logger.info(f"Tokenizer called | Client IP: {client_ip} | Tokens: {token_count}")
|
588 |
return {"text": text, "tokens": token_count}
|
589 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
590 |
if __name__ == "__main__":
|
591 |
import uvicorn
|
592 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|