Niansuh commited on
Commit
2117a04
·
verified ·
1 Parent(s): 8b177d4

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +310 -111
main.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import os
2
  import re
3
  import random
@@ -11,11 +13,15 @@ from collections import defaultdict
11
  from typing import List, Dict, Any, Optional, AsyncGenerator, Union
12
 
13
  from datetime import datetime
 
14
  from aiohttp import ClientSession, ClientTimeout, ClientError
15
  from fastapi import FastAPI, HTTPException, Request, Depends, Header
16
- from fastapi.responses import StreamingResponse, JSONResponse
17
  from pydantic import BaseModel
18
 
 
 
 
19
  # Configure logging
20
  logging.basicConfig(
21
  level=logging.INFO,
@@ -25,142 +31,109 @@ logging.basicConfig(
25
  logger = logging.getLogger(__name__)
26
 
27
  # Load environment variables
28
- API_KEYS = os.getenv('API_KEYS', '').split(',')
29
- RATE_LIMIT = int(os.getenv('RATE_LIMIT', '60'))
30
- AVAILABLE_MODELS = os.getenv('AVAILABLE_MODELS', '')
31
 
32
  if not API_KEYS or API_KEYS == ['']:
33
  logger.error("No API keys found. Please set the API_KEYS environment variable.")
34
  raise Exception("API_KEYS environment variable not set.")
35
 
 
36
  if AVAILABLE_MODELS:
37
  AVAILABLE_MODELS = [model.strip() for model in AVAILABLE_MODELS.split(',') if model.strip()]
38
  else:
39
- AVAILABLE_MODELS = []
40
 
 
41
  rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
42
- CLEANUP_INTERVAL = 60
43
- RATE_LIMIT_WINDOW = 60
 
 
44
 
45
  async def cleanup_rate_limit_stores():
 
 
 
46
  while True:
47
  current_time = time.time()
48
  ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
49
  for ip in ips_to_delete:
50
  del rate_limit_store[ip]
 
51
  await asyncio.sleep(CLEANUP_INTERVAL)
52
 
53
  async def rate_limiter_per_ip(request: Request):
 
 
 
54
  client_ip = request.client.host
55
  current_time = time.time()
56
 
 
57
  if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
58
  rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
59
  else:
60
  if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
61
- raise HTTPException(status_code=429, detail='Rate limit exceeded')
 
62
  rate_limit_store[client_ip]["count"] += 1
63
 
64
  async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
 
 
 
65
  client_ip = request.client.host
66
  if authorization is None or not authorization.startswith('Bearer '):
 
67
  raise HTTPException(status_code=401, detail='Invalid authorization header format')
68
  api_key = authorization[7:]
69
  if api_key not in API_KEYS:
 
70
  raise HTTPException(status_code=401, detail='Invalid API key')
71
  return api_key
72
 
73
- class ImageResponse:
74
- def __init__(self, url: str, alt: str):
75
- self.url = url
76
- self.alt = alt
77
-
78
- def to_data_uri(image_base64: str) -> str:
79
- return f"data:image/jpeg;base64,{image_base64}"
80
-
81
- class Blackbox:
82
- url = "https://www.blackbox.ai"
83
- api_endpoint = "https://www.blackbox.ai/api/chat"
84
- working = True
85
- supports_stream = True
86
-
87
- default_model = 'blackboxai'
88
- models = [default_model, 'ImageGeneration', 'gpt-4o', 'llama-3.1-8b']
89
-
90
- @classmethod
91
- def get_model(cls, model: str) -> Optional[str]:
92
- if model in cls.models:
93
- return model
94
- else:
95
- return cls.default_model
96
-
97
- @classmethod
98
- async def create_async_generator(
99
- cls,
100
- model: str,
101
- messages: List[Dict[str, str]],
102
- image_base64: Optional[str] = None,
103
- **kwargs
104
- ) -> AsyncGenerator[Any, None]:
105
- model = cls.get_model(model)
106
- if model is None:
107
- raise HTTPException(status_code=400, detail="Model not available")
108
-
109
- headers = {
110
- "accept": "*/*",
111
- "content-type": "application/json",
112
- "origin": cls.url,
113
- "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
114
- "referer": f"{cls.url}/?model={model}"
115
- }
116
-
117
- random_id = ''.join(random.choices(string.ascii_letters + string.digits, k=7))
118
- data = {
119
- "messages": messages,
120
- "id": random_id,
121
- "previewToken": None,
122
- "userId": None,
123
- "codeModelMode": True,
124
- "agentMode": {},
125
- "trendingAgentMode": {},
126
- "isMicMode": False,
127
- "userSystemPrompt": None,
128
- "maxTokens": 1024,
129
- "playgroundTopP": 0.9,
130
- "playgroundTemperature": 0.5,
131
- "isChromeExt": False,
132
- "githubToken": None,
133
- "clickedAnswer2": False,
134
- "clickedAnswer3": False,
135
- "clickedForceWebSearch": False,
136
- "visitFromDelta": False,
137
- "mobileClient": False,
138
- "userSelectedModel": model,
139
- "webSearchMode": False,
140
- }
141
-
142
- if image_base64:
143
- data["messages"][-1]['data'] = {
144
- 'imageBase64': to_data_uri(image_base64),
145
- 'fileText': '',
146
- 'title': 'Uploaded Image'
147
- }
148
- data["messages"][-1]['content'] = 'FILE:BB\n$#$\n\n$#$\n' + data["messages"][-1]['content']
149
-
150
- timeout = ClientTimeout(total=60)
151
- async with ClientSession(headers=headers, timeout=timeout) as session:
152
- async with session.post(cls.api_endpoint, json=data) as response:
153
- response.raise_for_status()
154
- async for chunk in response.content.iter_any():
155
- decoded_chunk = chunk.decode(errors='ignore')
156
- yield decoded_chunk
157
 
 
158
  app = FastAPI()
159
 
 
160
  @app.on_event("startup")
161
  async def startup_event():
162
  asyncio.create_task(cleanup_rate_limit_stores())
 
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  class Message(BaseModel):
165
  role: str
166
  content: str
@@ -168,38 +141,264 @@ class Message(BaseModel):
168
  class ChatRequest(BaseModel):
169
  model: str
170
  messages: List[Message]
171
- image_base64: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
174
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
175
- try:
176
- messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
 
 
 
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  async_generator = Blackbox.create_async_generator(
179
  model=request.model,
180
- messages=messages,
181
- image_base64=request.image_base64
 
 
 
182
  )
183
 
184
- response_content = ""
185
- async for chunk in async_generator:
186
- response_content += chunk
187
-
188
- return {"response": response_content}
189
-
190
- except HTTPException as e:
191
- raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  except Exception as e:
193
- raise HTTPException(status_code=500, detail="Internal Server Error")
194
-
 
 
 
 
 
 
 
 
 
 
 
195
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
196
- async def get_models():
 
 
197
  return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
198
 
199
- @app.get("/v1/health")
200
- async def health_check():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  return {"status": "ok"}
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if __name__ == "__main__":
204
  import uvicorn
205
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ # app/main.py
2
+
3
  import os
4
  import re
5
  import random
 
13
  from typing import List, Dict, Any, Optional, AsyncGenerator, Union
14
 
15
  from datetime import datetime
16
+
17
  from aiohttp import ClientSession, ClientTimeout, ClientError
18
  from fastapi import FastAPI, HTTPException, Request, Depends, Header
19
+ from fastapi.responses import StreamingResponse, JSONResponse, RedirectResponse
20
  from pydantic import BaseModel
21
 
22
+ from .blackbox import Blackbox, ImageResponse
23
+ from .image import to_data_uri, ImageType
24
+
25
  # Configure logging
26
  logging.basicConfig(
27
  level=logging.INFO,
 
31
  logger = logging.getLogger(__name__)
32
 
33
  # Load environment variables
34
+ API_KEYS = os.getenv('API_KEYS', '').split(',') # Comma-separated API keys
35
+ RATE_LIMIT = int(os.getenv('RATE_LIMIT', '60')) # Requests per minute
36
+ AVAILABLE_MODELS = os.getenv('AVAILABLE_MODELS', '') # Comma-separated available models
37
 
38
  if not API_KEYS or API_KEYS == ['']:
39
  logger.error("No API keys found. Please set the API_KEYS environment variable.")
40
  raise Exception("API_KEYS environment variable not set.")
41
 
42
+ # Process available models
43
  if AVAILABLE_MODELS:
44
  AVAILABLE_MODELS = [model.strip() for model in AVAILABLE_MODELS.split(',') if model.strip()]
45
  else:
46
+ AVAILABLE_MODELS = [] # If empty, all models are available
47
 
48
+ # Simple in-memory rate limiter based solely on IP addresses
49
  rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
50
+
51
+ # Define cleanup interval and window
52
+ CLEANUP_INTERVAL = 60 # seconds
53
+ RATE_LIMIT_WINDOW = 60 # seconds
54
 
55
  async def cleanup_rate_limit_stores():
56
+ """
57
+ Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
58
+ """
59
  while True:
60
  current_time = time.time()
61
  ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
62
  for ip in ips_to_delete:
63
  del rate_limit_store[ip]
64
+ logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
65
  await asyncio.sleep(CLEANUP_INTERVAL)
66
 
67
  async def rate_limiter_per_ip(request: Request):
68
+ """
69
+ Rate limiter that enforces a limit based on the client's IP address.
70
+ """
71
  client_ip = request.client.host
72
  current_time = time.time()
73
 
74
+ # Initialize or update the count and timestamp
75
  if current_time - rate_limit_store[client_ip]["timestamp"] > RATE_LIMIT_WINDOW:
76
  rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
77
  else:
78
  if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
79
+ logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
80
+ raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
81
  rate_limit_store[client_ip]["count"] += 1
82
 
83
  async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
84
+ """
85
+ Dependency to extract and validate the API key from the Authorization header.
86
+ """
87
  client_ip = request.client.host
88
  if authorization is None or not authorization.startswith('Bearer '):
89
+ logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
90
  raise HTTPException(status_code=401, detail='Invalid authorization header format')
91
  api_key = authorization[7:]
92
  if api_key not in API_KEYS:
93
+ logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
94
  raise HTTPException(status_code=401, detail='Invalid API key')
95
  return api_key
96
 
97
+ # Custom exception for model not working
98
+ class ModelNotWorkingException(Exception):
99
+ def __init__(self, model: str):
100
+ self.model = model
101
+ self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
102
+ super().__init__(self.message)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ # FastAPI app setup
105
  app = FastAPI()
106
 
107
+ # Add the cleanup task when the app starts
108
  @app.on_event("startup")
109
  async def startup_event():
110
  asyncio.create_task(cleanup_rate_limit_stores())
111
+ logger.info("Started rate limit store cleanup task.")
112
 
113
+ # Middleware to enhance security and enforce Content-Type for specific endpoints
114
+ @app.middleware("http")
115
+ async def security_middleware(request: Request, call_next):
116
+ client_ip = request.client.host
117
+ # Enforce that POST requests to /v1/chat/completions must have Content-Type: application/json
118
+ if request.method == "POST" and request.url.path == "/v1/chat/completions":
119
+ content_type = request.headers.get("Content-Type")
120
+ if content_type != "application/json":
121
+ logger.warning(f"Invalid Content-Type from IP: {client_ip} for path: {request.url.path}")
122
+ return JSONResponse(
123
+ status_code=400,
124
+ content={
125
+ "error": {
126
+ "message": "Content-Type must be application/json",
127
+ "type": "invalid_request_error",
128
+ "param": None,
129
+ "code": None
130
+ }
131
+ },
132
+ )
133
+ response = await call_next(request)
134
+ return response
135
+
136
+ # Request Models
137
  class Message(BaseModel):
138
  role: str
139
  content: str
 
141
  class ChatRequest(BaseModel):
142
  model: str
143
  messages: List[Message]
144
+ temperature: Optional[float] = 1.0
145
+ top_p: Optional[float] = 1.0
146
+ n: Optional[int] = 1
147
+ stream: Optional[bool] = False
148
+ stop: Optional[Union[str, List[str]]] = None
149
+ max_tokens: Optional[int] = None
150
+ presence_penalty: Optional[float] = 0.0
151
+ frequency_penalty: Optional[float] = 0.0
152
+ logit_bias: Optional[Dict[str, float]] = None
153
+ user: Optional[str] = None
154
+ webSearchMode: Optional[bool] = False # Custom parameter
155
+ image: Optional[str] = None # Base64-encoded image
156
+
157
+ class TokenizerRequest(BaseModel):
158
+ text: str
159
+
160
+ def calculate_estimated_cost(prompt_tokens: int, completion_tokens: int) -> float:
161
+ """
162
+ Calculate the estimated cost based on the number of tokens.
163
+ Replace the pricing below with your actual pricing model.
164
+ """
165
+ # Example pricing: $0.00000268 per token
166
+ cost_per_token = 0.00000268
167
+ return round((prompt_tokens + completion_tokens) * cost_per_token, 8)
168
+
169
+ def create_response(content: str, model: str, finish_reason: Optional[str] = None) -> Dict[str, Any]:
170
+ return {
171
+ "id": f"chatcmpl-{uuid.uuid4()}",
172
+ "object": "chat.completion",
173
+ "created": int(datetime.now().timestamp()),
174
+ "model": model,
175
+ "choices": [
176
+ {
177
+ "index": 0,
178
+ "message": {
179
+ "role": "assistant",
180
+ "content": content
181
+ },
182
+ "finish_reason": finish_reason
183
+ }
184
+ ],
185
+ "usage": None, # To be filled in non-streaming responses
186
+ }
187
 
188
  @app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
189
  async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
190
+ client_ip = req.client.host
191
+ # Redact user messages only for logging purposes
192
+ redacted_messages = [{"role": msg.role, "content": "[redacted]"} for msg in request.messages]
193
+
194
+ logger.info(f"Received chat completions request from API key: {api_key} | IP: {client_ip} | Model: {request.model} | Messages: {redacted_messages}")
195
 
196
+ try:
197
+ # Validate that the requested model is available
198
+ if request.model not in Blackbox.models and request.model not in Blackbox.model_aliases:
199
+ logger.warning(f"Attempt to use unavailable model: {request.model} from IP: {client_ip}")
200
+ raise HTTPException(status_code=400, detail="Requested model is not available.")
201
+
202
+ # Process the image if provided
203
+ image_data = None
204
+ image_name = None
205
+ if request.image:
206
+ try:
207
+ # Validate and process the base64 image
208
+ image_data = to_data_uri(request.image)
209
+ image_name = "uploaded_image"
210
+ logger.info(f"Image data received and processed from IP: {client_ip}")
211
+ except Exception as e:
212
+ logger.error(f"Image processing failed: {e}")
213
+ raise HTTPException(status_code=400, detail="Invalid image data provided.")
214
+
215
+ # Process the request with actual message content, but don't log it
216
  async_generator = Blackbox.create_async_generator(
217
  model=request.model,
218
+ messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], # Actual message content used here
219
+ proxy=None,
220
+ image=image_data,
221
+ image_name=image_name,
222
+ webSearchMode=request.webSearchMode
223
  )
224
 
225
+ if request.stream:
226
+ async def generate():
227
+ try:
228
+ assistant_content = ""
229
+ async for chunk in async_generator:
230
+ if isinstance(chunk, ImageResponse):
231
+ # Handle image responses if necessary
232
+ image_markdown = f"![image]({chunk.url})\n"
233
+ assistant_content += image_markdown
234
+ response_chunk = create_response(image_markdown, request.model, finish_reason=None)
235
+ else:
236
+ assistant_content += chunk
237
+ # Yield the chunk as a partial choice
238
+ response_chunk = {
239
+ "id": f"chatcmpl-{uuid.uuid4()}",
240
+ "object": "chat.completion.chunk",
241
+ "created": int(datetime.now().timestamp()),
242
+ "model": request.model,
243
+ "choices": [
244
+ {
245
+ "index": 0,
246
+ "delta": {"content": chunk, "role": "assistant"},
247
+ "finish_reason": None,
248
+ }
249
+ ],
250
+ "usage": None, # Usage can be updated if you track tokens in real-time
251
+ }
252
+ yield f"data: {json.dumps(response_chunk)}\n\n"
253
+
254
+ # After all chunks are sent, send the final message with finish_reason
255
+ prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
256
+ completion_tokens = len(assistant_content.split())
257
+ total_tokens = prompt_tokens + completion_tokens
258
+ estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
259
+
260
+ final_response = {
261
+ "id": f"chatcmpl-{uuid.uuid4()}",
262
+ "object": "chat.completion",
263
+ "created": int(datetime.now().timestamp()),
264
+ "model": request.model,
265
+ "choices": [
266
+ {
267
+ "message": {
268
+ "role": "assistant",
269
+ "content": assistant_content
270
+ },
271
+ "finish_reason": "stop",
272
+ "index": 0
273
+ }
274
+ ],
275
+ "usage": {
276
+ "prompt_tokens": prompt_tokens,
277
+ "completion_tokens": completion_tokens,
278
+ "total_tokens": total_tokens,
279
+ "estimated_cost": estimated_cost
280
+ },
281
+ }
282
+ yield f"data: {json.dumps(final_response)}\n\n"
283
+ yield "data: [DONE]\n\n"
284
+ except HTTPException as he:
285
+ error_response = {"error": he.detail}
286
+ yield f"data: {json.dumps(error_response)}\n\n"
287
+ except Exception as e:
288
+ logger.exception(f"Error during streaming response generation from IP: {client_ip}.")
289
+ error_response = {"error": str(e)}
290
+ yield f"data: {json.dumps(error_response)}\n\n"
291
+
292
+ return StreamingResponse(generate(), media_type="text/event-stream")
293
+ else:
294
+ response_content = ""
295
+ async for chunk in async_generator:
296
+ if isinstance(chunk, ImageResponse):
297
+ response_content += f"![image]({chunk.url})\n"
298
+ else:
299
+ response_content += chunk
300
+
301
+ prompt_tokens = sum(len(msg.content.split()) for msg in request.messages)
302
+ completion_tokens = len(response_content.split())
303
+ total_tokens = prompt_tokens + completion_tokens
304
+ estimated_cost = calculate_estimated_cost(prompt_tokens, completion_tokens)
305
+
306
+ logger.info(f"Completed non-streaming response generation for API key: {api_key} | IP: {client_ip}")
307
+
308
+ return {
309
+ "id": f"chatcmpl-{uuid.uuid4()}",
310
+ "object": "chat.completion",
311
+ "created": int(datetime.now().timestamp()),
312
+ "model": request.model,
313
+ "choices": [
314
+ {
315
+ "message": {
316
+ "role": "assistant",
317
+ "content": response_content
318
+ },
319
+ "finish_reason": "stop",
320
+ "index": 0
321
+ }
322
+ ],
323
+ "usage": {
324
+ "prompt_tokens": prompt_tokens,
325
+ "completion_tokens": completion_tokens,
326
+ "total_tokens": total_tokens,
327
+ "estimated_cost": estimated_cost
328
+ },
329
+ }
330
+ except ModelNotWorkingException as e:
331
+ logger.warning(f"Model not working: {e} | IP: {client_ip}")
332
+ raise HTTPException(status_code=503, detail=str(e))
333
+ except HTTPException as he:
334
+ logger.warning(f"HTTPException: {he.detail} | IP: {client_ip}")
335
+ raise he
336
  except Exception as e:
337
+ logger.exception(f"An unexpected error occurred while processing the chat completions request from IP: {client_ip}.")
338
+ raise HTTPException(status_code=500, detail=str(e))
339
+
340
+ # Endpoint: POST /v1/tokenizer
341
+ @app.post("/v1/tokenizer", dependencies=[Depends(rate_limiter_per_ip)])
342
+ async def tokenizer(request: TokenizerRequest, req: Request):
343
+ client_ip = req.client.host
344
+ text = request.text
345
+ token_count = len(text.split())
346
+ logger.info(f"Tokenizer requested from IP: {client_ip} | Text length: {len(text)}")
347
+ return {"text": text, "tokens": token_count}
348
+
349
+ # Endpoint: GET /v1/models
350
  @app.get("/v1/models", dependencies=[Depends(rate_limiter_per_ip)])
351
+ async def get_models(req: Request):
352
+ client_ip = req.client.host
353
+ logger.info(f"Fetching available models from IP: {client_ip}")
354
  return {"data": [{"id": model, "object": "model"} for model in Blackbox.models]}
355
 
356
+ # Endpoint: GET /v1/models/{model}/status
357
+ @app.get("/v1/models/{model}/status", dependencies=[Depends(rate_limiter_per_ip)])
358
+ async def model_status(model: str, req: Request):
359
+ client_ip = req.client.host
360
+ logger.info(f"Model status requested for '{model}' from IP: {client_ip}")
361
+ if model in Blackbox.models:
362
+ return {"model": model, "status": "available"}
363
+ elif model in Blackbox.model_aliases and Blackbox.model_aliases[model] in Blackbox.models:
364
+ actual_model = Blackbox.model_aliases[model]
365
+ return {"model": actual_model, "status": "available via alias"}
366
+ else:
367
+ logger.warning(f"Model not found: {model} from IP: {client_ip}")
368
+ raise HTTPException(status_code=404, detail="Model not found")
369
+
370
+ # Endpoint: GET /v1/health
371
+ @app.get("/v1/health", dependencies=[Depends(rate_limiter_per_ip)])
372
+ async def health_check(req: Request):
373
+ client_ip = req.client.host
374
+ logger.info(f"Health check requested from IP: {client_ip}")
375
  return {"status": "ok"}
376
 
377
+ # Endpoint: GET /v1/chat/completions (GET method)
378
+ @app.get("/v1/chat/completions")
379
+ async def chat_completions_get(req: Request):
380
+ client_ip = req.client.host
381
+ logger.info(f"GET request made to /v1/chat/completions from IP: {client_ip}, redirecting to 'about:blank'")
382
+ return RedirectResponse(url='about:blank')
383
+
384
+ # Custom exception handler to match OpenAI's error format
385
+ @app.exception_handler(HTTPException)
386
+ async def http_exception_handler(request: Request, exc: HTTPException):
387
+ client_ip = request.client.host
388
+ logger.error(f"HTTPException: {exc.detail} | Path: {request.url.path} | IP: {client_ip}")
389
+ return JSONResponse(
390
+ status_code=exc.status_code,
391
+ content={
392
+ "error": {
393
+ "message": exc.detail,
394
+ "type": "invalid_request_error",
395
+ "param": None,
396
+ "code": None
397
+ }
398
+ },
399
+ )
400
+
401
+ # Run the application
402
  if __name__ == "__main__":
403
  import uvicorn
404
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)