kevin commited on
Commit
e3d6247
·
1 Parent(s): dab50b2

qwenchat2api

Browse files
Files changed (5) hide show
  1. .gitignore +1 -0
  2. Dockerfile +8 -0
  3. README.md +1 -0
  4. app.py +287 -0
  5. requirements.txt +4 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .idea
Dockerfile ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt /app/
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . /app/
6
+ RUN touch /app/app.log && chmod 777 /app/app.log
7
+ EXPOSE 8080
8
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
README.md CHANGED
@@ -5,6 +5,7 @@ colorFrom: red
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
5
  colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8080
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import traceback
4
+ from fastapi import FastAPI, HTTPException, Request, Depends, Response
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import StreamingResponse, JSONResponse
7
+ import httpx
8
+ import logging
9
+ import random
10
+ import uvicorn
11
+ import asyncio
12
+
13
+ QWEN_API_URL = 'https://chat.qwenlm.ai/api/chat/completions'
14
+ QWEN_MODELS_URL = 'https://chat.qwenlm.ai/api/models'
15
+ MAX_RETRIES = 3
16
+ RETRY_DELAY = 1
17
+
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format="%(asctime)s - %(levelname)s - %(message)s",
21
+ )
22
+ logger = logging.getLogger(__name__)
23
+
24
+ app = FastAPI()
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ client = httpx.AsyncClient()
35
+
36
+ async def fetch_with_retry(url, options, retries=MAX_RETRIES):
37
+ last_error = None
38
+ auth_header = options.get('headers', {}).get('Authorization', '')
39
+ if auth_header.startswith('Bearer '):
40
+ logger.info(f"Processing request with session identifier: {auth_header[7:]}")
41
+
42
+ for i in range(retries):
43
+ try:
44
+ async with httpx.AsyncClient() as client:
45
+ if 'headers' in options:
46
+ client.headers.update(options['headers'])
47
+
48
+ response = await client.request(
49
+ method=options.get('method', 'GET'),
50
+ url=url,
51
+ content=options.get('body'),
52
+ timeout=60
53
+ )
54
+
55
+ content_type = response.headers.get('content-type', '')
56
+
57
+ if 'text/html' in content_type or response.status_code == 500:
58
+ last_error = {
59
+ 'status': response.status_code,
60
+ 'contentType': content_type,
61
+ 'responseText': response.text[:1000],
62
+ 'headers': dict(response.headers)
63
+ }
64
+ if i < retries - 1:
65
+ logger.error(f"Retry attempt {i+1} for session {auth_header[7:]} failed")
66
+ await asyncio.sleep(RETRY_DELAY * (i + 1))
67
+ continue
68
+
69
+ return response
70
+
71
+ except Exception as error:
72
+ last_error = error
73
+ logger.error(f"Connection error for session {auth_header[7:]} on attempt {i+1}")
74
+ traceback.print_exc()
75
+ if i < retries - 1:
76
+ await asyncio.sleep(RETRY_DELAY * (i + 1))
77
+ continue
78
+
79
+ raise Exception(json.dumps({
80
+ 'error': True,
81
+ 'message': 'All retry attempts failed',
82
+ 'lastError': str(last_error),
83
+ 'retries': retries
84
+ }))
85
+
86
+ async def process_line(line, previous_content):
87
+ try:
88
+ data = json.loads(line[6:])
89
+ if data.get('choices') and data['choices'][0].get('delta') and data['choices'][0]['delta'].get('content'):
90
+ current_content = data['choices'][0]['delta']['content']
91
+ new_content = current_content
92
+ if current_content.startswith(previous_content) and len(previous_content) > 0:
93
+ new_content = current_content[len(previous_content):]
94
+
95
+ new_data = {
96
+ **data,
97
+ 'choices': [{
98
+ **data['choices'][0],
99
+ 'delta': {
100
+ **data['choices'][0]['delta'],
101
+ 'content': new_content
102
+ }
103
+ }]
104
+ }
105
+ yield f"data: {json.dumps(new_data)}\n\n"
106
+ else:
107
+ yield f"data: {json.dumps(data)}\n\n"
108
+
109
+ except Exception:
110
+ yield f"{line}\n\n"
111
+
112
+ async def handle_stream(response, previous_content):
113
+ buffer = ''
114
+ try:
115
+ async for chunk in response.aiter_bytes():
116
+ decoded_chunk = chunk.decode('utf-8')
117
+ buffer += decoded_chunk
118
+
119
+ lines = buffer.split('\n')
120
+ buffer = lines.pop() or ''
121
+
122
+ for line in lines:
123
+ if line.strip().startswith('data: '):
124
+ async for processed_chunk in process_line(line, previous_content):
125
+ yield processed_chunk
126
+ result = processed_chunk.split("\n\n")[0].replace("data: ", "")
127
+ result = json.loads(result)['choices'][0]['delta']['content']
128
+ if result:
129
+ previous_content = previous_content + result
130
+
131
+ yield "data: [DONE]\n\n"
132
+
133
+ except Exception as error:
134
+ yield f"data: {{\"error\":true,\"message\":\"{str(error)}\"}}\n\n"
135
+ yield "data: [DONE]\n\n"
136
+
137
+ async def get_openai_auth_headers(request: Request) -> dict:
138
+ auth_header = request.headers.get("Authorization")
139
+ if not auth_header:
140
+ raise HTTPException(status_code=401, detail="Missing Authorization header")
141
+ logger.info(f"New request authenticated with session {auth_header[7:]}")
142
+ return {"Authorization": auth_header}
143
+
144
+ async def make_request(method, url, headers, body, api_keys=None, retry_count=0):
145
+ try:
146
+ if api_keys and len(api_keys) > 1:
147
+ remaining_keys = api_keys.copy()
148
+ while remaining_keys and retry_count < 3:
149
+ selected_key = random.choice(remaining_keys)
150
+ remaining_keys.remove(selected_key)
151
+ headers = {**headers, "Authorization": f"Bearer {selected_key}"}
152
+ logger.info(f"Attempting request with API key: {selected_key}")
153
+
154
+ try:
155
+ async with httpx.AsyncClient() as client:
156
+
157
+ r = await client.request(
158
+ method,
159
+ url,
160
+ headers=headers,
161
+ content=body,
162
+ timeout=600
163
+ )
164
+ if r.status_code < 400:
165
+ return r
166
+ logger.error(f"Request failed with key {selected_key}, status code: {r.status_code}")
167
+ except Exception as e:
168
+ logger.error(f"Request failed with key {selected_key}: {str(e)}")
169
+ retry_count += 1
170
+
171
+ raise HTTPException(status_code=500, detail="All API keys failed")
172
+ else:
173
+ while retry_count < 3:
174
+ single_key = api_keys[0] if api_keys else headers.get("authorization", "").replace("Bearer ", "").strip()
175
+ request_headers = {**headers, "Authorization": f"Bearer {single_key}"}
176
+ logger.info(f"Attempting request with API key: {single_key}")
177
+ try:
178
+ async with httpx.AsyncClient() as client:
179
+ r = await client.request(
180
+ method,
181
+ url,
182
+ headers=request_headers,
183
+ content=body,
184
+ timeout=600
185
+ )
186
+ if r.status_code < 400:
187
+ return r
188
+ logger.error(f"Request attempt {retry_count + 1} failed for session {single_key}")
189
+ except Exception as e:
190
+ logger.error(f"Connection attempt {retry_count + 1} failed for session {single_key}")
191
+ retry_count += 1
192
+
193
+ raise HTTPException(status_code=500, detail="Request failed after 3 retries")
194
+
195
+ except Exception as e:
196
+ logger.error(f"Request failed: {str(e)}")
197
+ raise HTTPException(status_code=500, detail=str(e))
198
+
199
+ @app.api_route(
200
+ "/api/chat/completions",
201
+ methods=["POST", "OPTIONS"],
202
+ )
203
+ async def chat_completions(request: Request, auth_headers: dict = Depends(get_openai_auth_headers)):
204
+ headers = dict(request.headers)
205
+ if "content-length" in headers:
206
+ del headers["content-length"]
207
+ if "host" in headers:
208
+ del headers["host"]
209
+
210
+ request_body = await request.body()
211
+
212
+ try:
213
+ request_data = json.loads(request_body.decode('utf-8'))
214
+ except json.JSONDecodeError:
215
+ raise HTTPException(status_code=400, detail="Invalid JSON")
216
+
217
+ messages = request_data.get('messages')
218
+ stream = request_data.get('stream', False)
219
+ model = request_data.get('model')
220
+ max_tokens = request_data.get('max_tokens')
221
+
222
+ if not model:
223
+ raise HTTPException(status_code=400, detail="Model parameter is required")
224
+
225
+ qwen_request = {
226
+ 'model': model,
227
+ 'messages': messages,
228
+ 'stream': stream
229
+ }
230
+
231
+ if max_tokens is not None:
232
+ qwen_request['max_tokens'] = max_tokens
233
+
234
+ try:
235
+ response = await fetch_with_retry(QWEN_API_URL, {
236
+ 'method': 'POST',
237
+ 'headers': {
238
+ 'Content-Type': 'application/json',
239
+ **auth_headers
240
+ },
241
+ 'body': json.dumps(qwen_request),
242
+ 'stream': stream
243
+ })
244
+
245
+ if stream:
246
+ previous_content = ''
247
+ return StreamingResponse(handle_stream(response, previous_content), media_type="text/event-stream")
248
+ else:
249
+ return Response(content=response.content, status_code=response.status_code, headers=response.headers)
250
+
251
+ except Exception as error:
252
+ raise HTTPException(status_code=500, detail=str(error))
253
+
254
+ @app.get("/api/models")
255
+ async def models(request: Request, auth_headers: dict = Depends(get_openai_auth_headers)):
256
+ try:
257
+ response = await fetch_with_retry(QWEN_MODELS_URL, {
258
+ 'method': 'GET',
259
+ 'headers': {
260
+ 'Content-Type': 'application/json',
261
+ **auth_headers
262
+ },
263
+ 'timeout': 30
264
+ })
265
+
266
+ response_data = response.json()
267
+ return JSONResponse(content=response_data)
268
+
269
+ except Exception as e:
270
+ logger.error(f"Error in /api/models: {str(e)}")
271
+ raise HTTPException(status_code=500, detail=str(e))
272
+
273
+ @app.get('/')
274
+ async def index(request: Request):
275
+ return Response(status_code=302, headers={"Location": "https://chat.qwenlm.ai"})
276
+
277
+ @app.get('/{path:path}')
278
+ @app.post('/{path:path}')
279
+ async def redirect_all(path: str, request: Request):
280
+ if any(keyword in path.lower() for keyword in ['php', 'admin', 'login', 'wp-admin', 'manager', 'user', 'signin']):
281
+ return Response(status_code=301, headers={"Location": "http://127.0.0.1"})
282
+ if request.method == 'POST':
283
+ return Response(status_code=301, headers={"Location": "http://127.0.0.1"})
284
+ return Response(status_code=302, headers={"Location": "https://linux.do/u/f-droid"})
285
+
286
+ if __name__ == "__main__":
287
+ uvicorn.run(app, host="0.0.0.0", port=8080)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ httpx
4
+ python-multipart