Update main.py
Browse files
main.py
CHANGED
@@ -39,6 +39,22 @@ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
|
|
39 |
CLEANUP_INTERVAL = 60 # seconds
|
40 |
RATE_LIMIT_WINDOW = 60 # seconds
|
41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
class Blackbox:
|
43 |
label = "Blackbox AI"
|
44 |
url = "https://www.blackbox.ai"
|
@@ -327,121 +343,3 @@ class Blackbox:
|
|
327 |
error_text = f"Error {e.status}: {e.message}"
|
328 |
try:
|
329 |
error_response = await e.response.text()
|
330 |
-
cleaned_error = cls.clean_response(error_response)
|
331 |
-
error_text += f" - {cleaned_error}"
|
332 |
-
except Exception:
|
333 |
-
pass
|
334 |
-
raise HTTPException(status_code=e.status, detail=error_text)
|
335 |
-
except Exception as e:
|
336 |
-
raise HTTPException(status_code=500, detail=f"Unexpected error during /api/chat request: {str(e)}")
|
337 |
-
|
338 |
-
# FastAPI app setup
|
339 |
-
app = FastAPI()
|
340 |
-
|
341 |
-
# Add the cleanup task when the app starts
|
342 |
-
@app.on_event("startup")
|
343 |
-
async def startup_event():
|
344 |
-
asyncio.create_task(cleanup_rate_limit_stores())
|
345 |
-
logger.info("Started rate limit store cleanup task.")
|
346 |
-
|
347 |
-
# Middleware to enhance security and enforce Content-Type for specific endpoints
|
348 |
-
@app.middleware("http")
|
349 |
-
async def security_middleware(request: Request, call_next):
|
350 |
-
client_ip = request.client.host
|
351 |
-
# Enforce that POST requests to /v1/chat/completions must have Content-Type: application/json
|
352 |
-
if request.method == "POST" and request.url.path == "/v1/chat/completions":
|
353 |
-
content_type = request.headers.get("Content-Type")
|
354 |
-
if content_type != "application/json":
|
355 |
-
logger.warning(f"Invalid Content-Type from IP: {client_ip} for path: {request.url.path}")
|
356 |
-
return JSONResponse(
|
357 |
-
status_code=400,
|
358 |
-
content={
|
359 |
-
"error": {
|
360 |
-
"message": "Content-Type must be application/json",
|
361 |
-
"type": "invalid_request_error",
|
362 |
-
"param": None,
|
363 |
-
"code": None
|
364 |
-
}
|
365 |
-
},
|
366 |
-
)
|
367 |
-
response = await call_next(request)
|
368 |
-
return response
|
369 |
-
|
370 |
-
# Request Models
|
371 |
-
class Message(BaseModel):
|
372 |
-
role: str
|
373 |
-
content: str
|
374 |
-
|
375 |
-
class ChatRequest(BaseModel):
|
376 |
-
model: str
|
377 |
-
messages: List[Message]
|
378 |
-
temperature: Optional[float] = 1.0
|
379 |
-
top_p: Optional[float] = 1.0
|
380 |
-
n: Optional[int] = 1
|
381 |
-
max_tokens: Optional[int] = None
|
382 |
-
presence_penalty: Optional[float] = 0.0
|
383 |
-
frequency_penalty: Optional[float] = 0.0
|
384 |
-
logit_bias: Optional[Dict[str, float]] = None
|
385 |
-
user: Optional[str] = None
|
386 |
-
|
387 |
-
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
|
388 |
-
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
389 |
-
client_ip = req.client.host
|
390 |
-
# Redact user messages only for logging purposes
|
391 |
-
redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
|
392 |
-
|
393 |
-
logger.info(f"Received chat completions request from API key: {api_key} | IP: {client_ip} | Model: {request.model} | Messages: {redacted_messages}")
|
394 |
-
|
395 |
-
try:
|
396 |
-
# Validate that the requested model is available
|
397 |
-
if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
|
398 |
-
logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
|
399 |
-
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
400 |
-
|
401 |
-
# Process the request with actual message content, but don't log it
|
402 |
-
response_content = await Blackbox.generate_response(
|
403 |
-
model=request.model,
|
404 |
-
messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
|
405 |
-
temperature=request.temperature,
|
406 |
-
max_tokens=request.max_tokens
|
407 |
-
)
|
408 |
-
|
409 |
-
logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
|
410 |
-
return response_content
|
411 |
-
except HTTPException as he:
|
412 |
-
logger.warning(f"HTTPException: {he.detail} | IP: {client_ip}")
|
413 |
-
raise he
|
414 |
-
except Exception as e:
|
415 |
-
logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
|
416 |
-
raise HTTPException(status_code=500, detail=str(e))
|
417 |
-
|
418 |
-
# Endpoint: GET /v1/models
|
419 |
-
@app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
|
420 |
-
async def get_models(req: Request):
|
421 |
-
client_ip = req.client.host
|
422 |
-
logger.info(f"Fetching available models from IP: {client_ip}")
|
423 |
-
return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
|
424 |
-
|
425 |
-
# Endpoint: GET /v1/health
|
426 |
-
@app.get("/v1/health", dependencies=[Depends(rate_limiter_per_ip)])
|
427 |
-
async def health_check(req: Request):
|
428 |
-
client_ip = req.client.host
|
429 |
-
logger.info(f"Health check requested from IP: {client_ip}")
|
430 |
-
return {"status": "ok"}
|
431 |
-
|
432 |
-
# Custom exception handler to match OpenAI's error format
|
433 |
-
@app.exception_handler(HTTPException)
|
434 |
-
async def http_exception_handler(request: Request, exc: HTTPException):
|
435 |
-
client_ip = request.client.host
|
436 |
-
logger.error(f"HTTPException: {exc.detail} | Path: {request.url.path} | IP: {client_ip}")
|
437 |
-
return JSONResponse(
|
438 |
-
status_code=exc.status_code,
|
439 |
-
content={
|
440 |
-
"error": {
|
441 |
-
"message": exc.detail,
|
442 |
-
"type": "invalid_request_error",
|
443 |
-
"param": None,
|
444 |
-
"code": None
|
445 |
-
}
|
446 |
-
},
|
447 |
-
)
|
|
|
39 |
CLEANUP_INTERVAL = 60 # seconds
|
40 |
RATE_LIMIT_WINDOW = 60 # seconds
|
41 |
|
42 |
+
async def rate_limiter_per_ip(request: Request):
|
43 |
+
"""
|
44 |
+
Rate limiter that enforces a limit based on the client's IP address.
|
45 |
+
"""
|
46 |
+
client_ip = request.client.host
|
47 |
+
current_time = time.time()
|
48 |
+
|
49 |
+
# Initialize or update the count and timestamp
|
50 |
+
if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
|
51 |
+
rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
|
52 |
+
else:
|
53 |
+
if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
|
54 |
+
logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
|
55 |
+
raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address')
|
56 |
+
rate_limit_store[client_ip]["count"] += 1
|
57 |
+
|
58 |
class Blackbox:
|
59 |
label = "Blackbox AI"
|
60 |
url = "https://www.blackbox.ai"
|
|
|
343 |
error_text = f"Error {e.status}: {e.message}"
|
344 |
try:
|
345 |
error_response = await e.response.text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|