yangjianchuan commited on
Commit
96601d6
·
0 Parent(s):

first commit

Browse files
Files changed (6) hide show
  1. Dockerfile +21 -0
  2. README.md +104 -0
  3. index.html +96 -0
  4. qwen.py +299 -0
  5. requirements.txt +3 -0
  6. static/favicon.png +0 -0
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # 设置工作目录
4
+ WORKDIR /app
5
+
6
+ # 复制依赖文件
7
+ COPY requirements.txt .
8
+
9
+ # 安装依赖
10
+ RUN pip install -r requirements.txt
11
+
12
+ # 复制应用代码
13
+ COPY index.html .
14
+ COPY qwen.py .
15
+ COPY static ./static
16
+
17
+ # 暴露端口
18
+ EXPOSE 8000
19
+
20
+ # 启动命令
21
+ CMD ["python", "qwen.py"]
README.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Q
3
+ emoji: 🏢
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ app_port: 8000
10
+ ---
11
+
12
+ # 通义千问 API 代理服务器
13
+
14
+ 这是一个基于 FastAPI 实现的通义千问 API 代理服务器,用于转发和处理与通义千问 API 的通信。
15
+
16
+ ## 主要功能
17
+
18
+ - 模型列表获取 API
19
+ - 聊天完成 API
20
+ - 支持流式响应
21
+ - 内置模型列表缓存机制
22
+ - 自动重试机制
23
+
24
+ ## 环境要求
25
+
26
+ - Python 3.7+
27
+ - FastAPI 0.104.1+
28
+ - Uvicorn 0.24.0+
29
+ - HTTPX 0.25.1+
30
+
31
+ ## 安装步骤
32
+
33
+ 1. 克隆项目到本地
34
+
35
+ 2. 安装依赖:
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ ```
39
+
40
+ 3. 使用 Docker(可选):
41
+ ```bash
42
+ docker build -t qwen-api-proxy .
43
+ docker run -p 8000:8000 qwen-api-proxy
44
+ ```
45
+
46
+ ## 运行服务
47
+
48
+ ```bash
49
+ python qwen.py
50
+ ```
51
+ 或使用 uvicorn:
52
+ ```bash
53
+ uvicorn qwen:app --host 0.0.0.0 --port 8000
54
+ ```
55
+
56
+ 服务将在 http://localhost:8000 上运行。
57
+
58
+ ## API 接口说明
59
+
60
+ ### 1. 获取模型列表
61
+
62
+ ```
63
+ GET /api/models
64
+ Header: Authorization: Bearer <your-token>
65
+ ```
66
+
67
+ 返回可用的模型列表,结果会被缓存1小时。
68
+
69
+ ### 2. 聊天完成
70
+
71
+ ```
72
+ POST /api/chat/completions
73
+ Header: Authorization: Bearer <your-token>
74
+
75
+ {
76
+ "model": "string",
77
+ "messages": [
78
+ {
79
+ "role": "user",
80
+ "content": "string"
81
+ }
82
+ ],
83
+ "stream": boolean,
84
+ "max_tokens": number (可选)
85
+ }
86
+ ```
87
+
88
+ 支持流式和非流式响应,可以通过 stream 参数控制。
89
+
90
+ ## 错误处理
91
+
92
+ - 服务内置了自动重试机制,最多重试3次
93
+ - 500错误或HTML响应会触发重试
94
+ - 401错误表示未授权,需要检查token
95
+ - 400错误表示请求参数有误
96
+
97
+ ## 获取 API Key
98
+
99
+ 1. 访问 https://chat.qwenlm.ai/ 并登录
100
+ 2. 打开浏览器开发者工具(通常按 F12)
101
+ 3. 切换到"应用程序"选项卡
102
+ 4. 在左侧菜单中选择"Cookies" -> "https://chat.qwenlm.ai"
103
+ 5. 找到名称为"token"的cookie
104
+ 6. 复制其值,这就是你的API Key
index.html ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>API 接口说明</title>
5
+ <link rel="icon" type="image/png" href="/static/favicon.png">
6
+ <style>
7
+ body { font-family: Arial, sans-serif; margin: 20px; }
8
+ h1 { color: #333; }
9
+ .endpoint { margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 5px; }
10
+ .method { font-weight: bold; color: #007bff; }
11
+ .url { color: #28a745; }
12
+ .description { margin-top: 10px; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <h1>API 接口说明</h1>
17
+
18
+ <h2>项目概述</h2>
19
+ <p>这是一个基于 FastAPI 实现的通义千问 API 代理服务器,用于转发和处理与通义千问 API 的通信。</p>
20
+
21
+ <h2>主要功能</h2>
22
+ <ul>
23
+ <li>模型列表获取 API</li>
24
+ <li>聊天完成 API</li>
25
+ <li>支持流式响应</li>
26
+ <li>内置模型列表缓存机制</li>
27
+ <li>自动重试机制</li>
28
+ </ul>
29
+
30
+ <h2>环境要求</h2>
31
+ <ul>
32
+ <li>Python 3.7+</li>
33
+ <li>FastAPI 0.104.1+</li>
34
+ <li>Uvicorn 0.24.0+</li>
35
+ <li>HTTPX 0.25.1+</li>
36
+ </ul>
37
+
38
+ <h2>安装步骤</h2>
39
+ <ol>
40
+ <li>克隆项目到本地</li>
41
+ <li>安装依赖:
42
+ <pre><code>pip install -r requirements.txt</code></pre>
43
+ </li>
44
+ <li>使用 Docker(可选):
45
+ <pre><code>docker build -t qwen-api-proxy .
46
+ docker run -p 8000:8000 qwen-api-proxy</code></pre>
47
+ </li>
48
+ </ol>
49
+
50
+ <h2>运行服务</h2>
51
+ <pre><code>python qwen.py</code></pre>
52
+ <p>或使用 uvicorn:</p>
53
+ <pre><code>uvicorn qwen:app --host 0.0.0.0 --port 8000</code></pre>
54
+ <p>服务将在 <a href="http://localhost:8000">http://localhost:8000</a> 上运行。</p>
55
+
56
+ <h2>错误处理</h2>
57
+ <ul>
58
+ <li>服务内置了自动重试机制,最多重试3次</li>
59
+ <li>500错误或HTML响应会触发重试</li>
60
+ <li>401错误表示未授权,需要检查token</li>
61
+ <li>400错误表示请求参数有误</li>
62
+ </ul>
63
+
64
+ <h2>获取 API Key</h2>
65
+ <ol>
66
+ <li>访问 <a href="https://chat.qwenlm.ai/">https://chat.qwenlm.ai/</a> 并登录</li>
67
+ <li>打开浏览器开发者工具(通常按 F12)</li>
68
+ <li>切换到"应用程序"选项卡</li>
69
+ <li>在左侧菜单中选择"Cookies" -> "https://chat.qwenlm.ai"</li>
70
+ <li>找到名称为"token"的cookie</li>
71
+ <li>复制其值,这就是你的API Key</li>
72
+ </ol>
73
+
74
+ <h2>许可证</h2>
75
+ <p>本项目采用 MIT License 开源许可证。</p>
76
+
77
+ <div class="endpoint">
78
+ <div class="method">GET</div>
79
+ <div class="url">/api/models</div>
80
+ <div class="description">
81
+ 获取可用模型列表<br>
82
+ 请求头需要包含 Authorization: Bearer {api_key}
83
+ </div>
84
+ </div>
85
+
86
+ <div class="endpoint">
87
+ <div class="method">POST</div>
88
+ <div class="url">/api/chat/completions</div>
89
+ <div class="description">
90
+ 与模型进行对话<br>
91
+ 请求头需要包含 Authorization: Bearer {api_key}<br>
92
+ 支持流式响应(stream: true)
93
+ </div>
94
+ </div>
95
+ </body>
96
+ </html>
qwen.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Response, UploadFile, File
2
+ from fastapi.responses import StreamingResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import httpx
5
+ import json
6
+ import asyncio
7
+ import time
8
+ import base64
9
+ from typing import Optional, Dict, Any, List
10
+ from io import BytesIO
11
+
12
+ # 配置常量
13
+ QWEN_API_URL = "https://chat.qwenlm.ai/api/chat/completions"
14
+ QWEN_MODELS_URL = "https://chat.qwenlm.ai/api/models"
15
+ QWEN_FILES_URL = "https://chat.qwenlm.ai/api/v1/files/"
16
+ MAX_RETRIES = 3
17
+ RETRY_DELAY = 1 # 1秒
18
+
19
+ # 缓存设置
20
+ cached_models = None
21
+ cached_models_timestamp = 0
22
+ CACHE_TTL = 60 * 60 # 缓存1小时
23
+
24
+ app = FastAPI()
25
+ app.mount("/static", StaticFiles(directory="static"), name="static")
26
+ client = httpx.AsyncClient()
27
+
28
+ @app.get("/")
29
+ async def root():
30
+ return FileResponse("index.html")
31
+
32
+ async def sleep(seconds: float):
33
+ await asyncio.sleep(seconds)
34
+
35
+ # 添加 base64 转换为文件的函数
36
+ async def base64_to_file(base64_str: str) -> BytesIO:
37
+ try:
38
+ # 去除 data:image/jpeg;base64, 这样的前缀
39
+ if ',' in base64_str:
40
+ base64_str = base64_str.split(',', 1)[1]
41
+
42
+ # 解码 base64 数据
43
+ image_data = base64.b64decode(base64_str)
44
+ return BytesIO(image_data)
45
+ except Exception as e:
46
+ raise Exception(f"Failed to convert base64 to file: {str(e)}")
47
+
48
+ # 添加图片上传函数
49
+ async def upload_image_to_qwen(auth_header: str, image_data: BytesIO) -> str:
50
+ try:
51
+ files = {'file': ('image.jpg', image_data, 'image/jpeg')}
52
+ headers = {
53
+ "Authorization": auth_header,
54
+ "accept": "application/json"
55
+ }
56
+
57
+ async with httpx.AsyncClient() as client:
58
+ response = await client.post(
59
+ QWEN_FILES_URL,
60
+ headers=headers,
61
+ files=files
62
+ )
63
+
64
+ if response.is_success:
65
+ data = response.json()
66
+ if not data.get('id'):
67
+ raise Exception("File upload failed: No valid file ID returned")
68
+ return data['id']
69
+ else:
70
+ raise Exception(f"File upload failed with status {response.status_code}")
71
+
72
+ except Exception as e:
73
+ raise Exception(f"Failed to upload image: {str(e)}")
74
+
75
+ # 添加消息处理函数
76
+ async def process_messages(messages: List[Dict], auth_header: str) -> List[Dict]:
77
+ processed_messages = []
78
+
79
+ for message in messages:
80
+ if isinstance(message.get('content'), list):
81
+ new_content = []
82
+ for content in message['content']:
83
+ if (content.get('type') == 'image_url' and
84
+ content.get('image_url', {}).get('url', '').startswith('data:')):
85
+ # 处理 base64 图片
86
+ image_data = await base64_to_file(content['image_url']['url'])
87
+ image_id = await upload_image_to_qwen(auth_header, image_data)
88
+ new_content.append({
89
+ 'type': 'image',
90
+ 'image': image_id
91
+ })
92
+ else:
93
+ new_content.append(content)
94
+ message['content'] = new_content
95
+ processed_messages.append(message)
96
+
97
+ return processed_messages
98
+
99
+ async def fetch_with_retry(url: str, options: Dict, retries: int = MAX_RETRIES):
100
+ last_error = None
101
+
102
+ for i in range(retries):
103
+ try:
104
+ response = await client.request(
105
+ method=options.get("method", "GET"),
106
+ url=url,
107
+ headers=options.get("headers", {}),
108
+ json=options.get("json"),
109
+ )
110
+
111
+ if response.is_success:
112
+ return response
113
+
114
+ content_type = response.headers.get("content-type", "")
115
+ if response.status_code >= 500 or "text/html" in content_type:
116
+ last_error = {
117
+ "status": response.status_code,
118
+ "content_type": content_type,
119
+ "response_text": response.text[:1000],
120
+ "headers": dict(response.headers)
121
+ }
122
+
123
+ if i < retries - 1:
124
+ await sleep(RETRY_DELAY * (i + 1))
125
+ continue
126
+ else:
127
+ last_error = {
128
+ "status": response.status_code,
129
+ "headers": dict(response.headers)
130
+ }
131
+ break
132
+
133
+ except Exception as error:
134
+ last_error = error
135
+ if i < retries - 1:
136
+ await sleep(RETRY_DELAY * (i + 1))
137
+ continue
138
+
139
+ raise Exception(json.dumps({
140
+ "error": True,
141
+ "message": "All retry attempts failed",
142
+ "last_error": str(last_error),
143
+ "retries": retries
144
+ }))
145
+
146
+ async def process_line(line: str, previous_content: str) -> tuple[str, Optional[dict]]:
147
+ try:
148
+ data = json.loads(line[6:]) # 移除 "data: " 前缀
149
+ if (data.get("choices") and data["choices"][0].get("delta")):
150
+ delta = data["choices"][0]["delta"]
151
+ current_content = delta.get("content", "")
152
+
153
+ # 计算增量内容
154
+ if previous_content and current_content:
155
+ if current_content.startswith(previous_content):
156
+ new_content = current_content[len(previous_content):]
157
+ else:
158
+ new_content = current_content
159
+ else:
160
+ new_content = current_content
161
+
162
+ # 构造新的响应数据
163
+ new_data = {
164
+ "choices": [{
165
+ "delta": {
166
+ "role": delta.get("role", "assistant"),
167
+ "content": new_content
168
+ }
169
+ }]
170
+ }
171
+
172
+ return current_content, new_data
173
+ return previous_content, data
174
+ except:
175
+ return previous_content, None
176
+
177
+ async def stream_generator(response: httpx.Response):
178
+ buffer = ""
179
+ previous_content = ""
180
+
181
+ async for chunk in response.aiter_bytes():
182
+ chunk_text = chunk.decode()
183
+ buffer += chunk_text
184
+
185
+ lines = buffer.split("\n")
186
+ buffer = lines.pop() if lines else ""
187
+
188
+ for line in lines:
189
+ line = line.strip()
190
+ if line.startswith("data: "):
191
+ previous_content, data = await process_line(line, previous_content)
192
+ if data:
193
+ yield f"data: {json.dumps(data)}\n\n"
194
+
195
+ if buffer:
196
+ previous_content, data = await process_line(buffer, previous_content)
197
+ if data:
198
+ yield f"data: {json.dumps(data)}\n\n"
199
+
200
+ yield "data: [DONE]\n\n"
201
+
202
+ @app.get("/healthz")
203
+ async def health_check():
204
+ return {"status": "ok"}
205
+
206
+ @app.get("/api/models")
207
+ async def get_models(request: Request):
208
+ global cached_models, cached_models_timestamp
209
+
210
+ auth_header = request.headers.get("Authorization")
211
+ if not auth_header or not auth_header.startswith("Bearer "):
212
+ return Response(status_code=401, content="Unauthorized")
213
+
214
+ now = time.time()
215
+ if cached_models and now - cached_models_timestamp < CACHE_TTL:
216
+ return Response(
217
+ content=cached_models,
218
+ media_type="application/json"
219
+ )
220
+
221
+ try:
222
+ response = await fetch_with_retry(
223
+ QWEN_MODELS_URL,
224
+ {"headers": {"Authorization": auth_header}}
225
+ )
226
+
227
+ cached_models = response.text
228
+ cached_models_timestamp = now
229
+
230
+ return Response(
231
+ content=cached_models,
232
+ media_type="application/json"
233
+ )
234
+ except Exception as error:
235
+ return Response(
236
+ content=json.dumps({"error": True, "message": str(error)}),
237
+ status_code=500
238
+ )
239
+
240
+ @app.post("/api/chat/completions")
241
+ async def chat_completions(request: Request):
242
+ auth_header = request.headers.get("Authorization")
243
+ if not auth_header or not auth_header.startswith("Bearer "):
244
+ return Response(status_code=401, content="Unauthorized")
245
+
246
+ request_data = await request.json()
247
+ messages = request_data.get("messages")
248
+ stream = request_data.get("stream", False)
249
+ model = request_data.get("model")
250
+ max_tokens = request_data.get("max_tokens")
251
+
252
+ if not model:
253
+ return Response(
254
+ content=json.dumps({"error": True, "message": "Model parameter is required"}),
255
+ status_code=400
256
+ )
257
+
258
+ try:
259
+ # 处理消息中的图片
260
+ processed_messages = await process_messages(messages, auth_header)
261
+
262
+ qwen_request = {
263
+ "model": model,
264
+ "messages": processed_messages,
265
+ "stream": stream
266
+ }
267
+
268
+ if max_tokens is not None:
269
+ qwen_request["max_tokens"] = max_tokens
270
+
271
+ response = await client.post(
272
+ QWEN_API_URL,
273
+ headers={
274
+ "Content-Type": "application/json",
275
+ "Authorization": auth_header
276
+ },
277
+ json=qwen_request
278
+ )
279
+
280
+ if stream:
281
+ return StreamingResponse(
282
+ stream_generator(response),
283
+ media_type="text/event-stream"
284
+ )
285
+
286
+ return Response(
287
+ content=response.text,
288
+ media_type="application/json"
289
+ )
290
+
291
+ except Exception as error:
292
+ return Response(
293
+ content=json.dumps({"error": True, "message": str(error)}),
294
+ status_code=500
295
+ )
296
+
297
+ if __name__ == "__main__":
298
+ import uvicorn
299
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ httpx==0.25.1
static/favicon.png ADDED