Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from json import dumps, loads
|
3 |
+
from re import sub
|
4 |
+
from urllib.parse import urlparse, urlunparse
|
5 |
+
|
6 |
+
from fastapi import FastAPI, Request
|
7 |
+
from fastapi.responses import JSONResponse, StreamingResponse, HTMLResponse
|
8 |
+
from httpx import AsyncClient, Limits, Timeout
|
9 |
+
|
10 |
+
app = FastAPI(title='PROXI-API')
|
11 |
+
|
12 |
+
MODELS = [
|
13 |
+
'deepseek-ai/DeepSeek-R1',
|
14 |
+
'deepseek-ai/DeepSeek-V3',
|
15 |
+
'deepseek-ai/deepseek-llm-67b-chat',
|
16 |
+
'databricks/dbrx-instruct',
|
17 |
+
'Qwen/QwQ-32B-Preview',
|
18 |
+
'NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO'
|
19 |
+
]
|
20 |
+
|
21 |
+
MODELS_OBJ = {
|
22 |
+
'object': 'list',
|
23 |
+
'data': [
|
24 |
+
{
|
25 |
+
'id': model,
|
26 |
+
'object': 'model',
|
27 |
+
'created': int(datetime.now().timestamp()),
|
28 |
+
'owned_by': 'blackbox.ai'
|
29 |
+
}
|
30 |
+
for model in MODELS
|
31 |
+
]
|
32 |
+
}
|
33 |
+
|
34 |
+
BLOCKED_HEADERS = [
|
35 |
+
'x-forwarded-for',
|
36 |
+
'x-real-ip',
|
37 |
+
'proxy-authorization'
|
38 |
+
]
|
39 |
+
|
40 |
+
HTTPX_CLIENT_KWARGS = dict(
|
41 |
+
timeout=Timeout(
|
42 |
+
connect=15,
|
43 |
+
read=60,
|
44 |
+
write=30,
|
45 |
+
pool=30
|
46 |
+
),
|
47 |
+
limits=Limits(
|
48 |
+
max_keepalive_connections=10,
|
49 |
+
max_connections=100
|
50 |
+
),
|
51 |
+
follow_redirects=True,
|
52 |
+
http2=True,
|
53 |
+
verify=False
|
54 |
+
)
|
55 |
+
|
56 |
+
|
57 |
+
def normalize_headers(request: Request, original_url: str) -> dict:
|
58 |
+
headers = {
|
59 |
+
key: value for (key, value) in request.headers.items()
|
60 |
+
if key.lower() not in BLOCKED_HEADERS and key.lower() != 'host'
|
61 |
+
}
|
62 |
+
parsed_url = urlparse(original_url)
|
63 |
+
origin = urlunparse((parsed_url.scheme, parsed_url.netloc, '', '', '', ''))
|
64 |
+
normalized = {key.lower(): value for key, value in headers.items()}
|
65 |
+
normalized['referer'] = original_url
|
66 |
+
normalized['origin'] = origin
|
67 |
+
normalized['accept-encoding'] = 'deflate'
|
68 |
+
return normalized
|
69 |
+
|
70 |
+
|
71 |
+
def format_chunk(chunk: bytes, model: str) -> bytes | str | dict:
|
72 |
+
chunk_id = 'chatcmpl-AQ8Lzxlg8eSCB1lgVmboiXwZiexqE'
|
73 |
+
timestamp = int(datetime.now().timestamp())
|
74 |
+
data = {
|
75 |
+
"id": chunk_id,
|
76 |
+
"object": "chat.completion.chunk",
|
77 |
+
"created": timestamp,
|
78 |
+
"model": model,
|
79 |
+
"system_fingerprint": "fp_67802d9a6d",
|
80 |
+
"choices": [
|
81 |
+
{
|
82 |
+
"index": 0,
|
83 |
+
"delta": {
|
84 |
+
"content": chunk.decode()
|
85 |
+
},
|
86 |
+
"finish_reason": None
|
87 |
+
}
|
88 |
+
]
|
89 |
+
}
|
90 |
+
json_data = dumps(data, ensure_ascii=False)
|
91 |
+
str_data = f'data: {sub(r'\\\\([ntr])', r'\\\1', json_data)}\n\n'
|
92 |
+
return str_data
|
93 |
+
|
94 |
+
|
95 |
+
async def generate(request: Request, url: str, headers: dict, body: bytes):
|
96 |
+
body_str = body.decode('utf-8')
|
97 |
+
body_obj: dict = loads(body_str)
|
98 |
+
model = body_obj.get('model')
|
99 |
+
if 'max_tokens' not in body_obj:
|
100 |
+
body_obj['max_tokens'] = '32000'
|
101 |
+
body = dumps(body_obj, ensure_ascii=False).encode()
|
102 |
+
headers = dict(headers)
|
103 |
+
headers['content-length'] = str(len(body))
|
104 |
+
headers['content-type'] = 'application/json'
|
105 |
+
async with AsyncClient(**HTTPX_CLIENT_KWARGS) as stream_client:
|
106 |
+
async with stream_client.stream(method=request.method, url=url, headers=headers, content=body, cookies=request.cookies) as stream:
|
107 |
+
async for chunk in stream.aiter_raw():
|
108 |
+
yield format_chunk(chunk, model)
|
109 |
+
yield 'data: [DONE]\n\n'.encode()
|
110 |
+
|
111 |
+
|
112 |
+
@app.post('/api/chat/completions')
|
113 |
+
@app.post('/api/v1/chat/completions')
|
114 |
+
async def proxy(request: Request):
|
115 |
+
try:
|
116 |
+
url = 'https://api.blackbox.ai/api/chat'
|
117 |
+
headers = normalize_headers(request, url)
|
118 |
+
body = await request.body()
|
119 |
+
async with AsyncClient(**HTTPX_CLIENT_KWARGS) as headers_client:
|
120 |
+
async with headers_client.stream(method=request.method, url=url, headers=headers, content=body, cookies=request.cookies) as response:
|
121 |
+
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
|
122 |
+
filtered_headers = {
|
123 |
+
name: value for (name, value) in response.headers.items()
|
124 |
+
if name.lower() not in excluded_headers
|
125 |
+
}
|
126 |
+
return StreamingResponse(generate(request, url, headers, body), status_code=response.status_code, headers=filtered_headers)
|
127 |
+
|
128 |
+
except Exception as exc:
|
129 |
+
return JSONResponse({'error': response.status_code, 'body': f'{exc}'}, status_code=500)
|
130 |
+
|
131 |
+
|
132 |
+
@app.get('/')
|
133 |
+
@app.get('/api')
|
134 |
+
@app.get('/api/v1')
|
135 |
+
async def root():
|
136 |
+
return HTMLResponse('ну пролапс, ну и что', status_code=200)
|
137 |
+
|
138 |
+
|
139 |
+
@app.get('/api/models')
|
140 |
+
@app.get('/api/v1/models')
|
141 |
+
async def models():
|
142 |
+
return JSONResponse(MODELS_OBJ, status_code=200, media_type='application/json')
|
143 |
+
|
144 |
+
|
145 |
+
if __name__ == '__main__':
|
146 |
+
import uvicorn
|
147 |
+
port = 7860
|
148 |
+
uvicorn.run(app, host='0.0.0.0', port=port)
|