ZyphrZero commited on
Commit
d382277
·
1 Parent(s): 7d20731

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +300 -22
main.py CHANGED
@@ -12,8 +12,9 @@ import json
12
  import os
13
  import re
14
  import time
 
15
  from datetime import datetime
16
- from typing import Dict, List, Optional, Any, Union, Generator, Tuple
17
 
18
  import requests
19
  from fastapi import FastAPI, Request, Response, HTTPException, Header
@@ -31,6 +32,7 @@ class ServerConfig:
31
  # API Configuration
32
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
33
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
 
34
  BACKUP_TOKEN: str = os.getenv("BACKUP_TOKEN", "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ")
35
 
36
  # Model Configuration
@@ -179,12 +181,141 @@ class Model(BaseModel):
179
  owned_by: str
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  class ModelsResponse(BaseModel):
183
  """Models list response model"""
184
  object: str = "list"
185
  data: List[Model]
186
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  # =============================================================================
189
  # SSE Parser
190
  # =============================================================================
@@ -615,27 +746,6 @@ def get_auth_token() -> str:
615
  return ServerConfig.BACKUP_TOKEN
616
 
617
 
618
- def transform_thinking_content(content: str) -> str:
619
- """Transform thinking content according to configuration"""
620
- # Remove summary tags
621
- content = re.sub(r'(?s)<summary>.*?</summary>', '', content)
622
- # Clean up remaining tags
623
- content = content.replace("</thinking>", "").replace("<Full>", "").replace("</Full>", "")
624
- content = content.strip()
625
-
626
- if ServerConfig.THINKING_PROCESSING == "think":
627
- content = re.sub(r'<details[^>]*>', '<think>', content)
628
- content = content.replace("</details>", "</think>")
629
- elif ServerConfig.THINKING_PROCESSING == "strip":
630
- content = re.sub(r'<details[^>]*>', '', content)
631
- content = content.replace("</details>", "")
632
-
633
- # Remove line prefixes
634
- content = content.lstrip("> ")
635
- content = content.replace("\n> ", "\n")
636
-
637
- return content.strip()
638
-
639
 
640
  def create_openai_response_chunk(
641
  model: str,
@@ -1159,6 +1269,174 @@ async def chat_completions(
1159
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
1160
 
1161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1162
  # =============================================================================
1163
  # Main Entry Point
1164
  # =============================================================================
 
12
  import os
13
  import re
14
  import time
15
+ import uuid
16
  from datetime import datetime
17
+ from typing import Dict, List, Optional, Any, Union, Generator, Tuple, Literal
18
 
19
  import requests
20
  from fastapi import FastAPI, Request, Response, HTTPException, Header
 
32
  # API Configuration
33
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
34
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
35
+ ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY", AUTH_TOKEN)
36
  BACKUP_TOKEN: str = os.getenv("BACKUP_TOKEN", "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ")
37
 
38
  # Model Configuration
 
181
  owned_by: str
182
 
183
 
184
+ # ANTHROPIC API 兼容性模型
185
+ class ContentBlock(BaseModel):
186
+ type: str
187
+ text: str
188
+
189
+
190
+ class AnthropicMessage(BaseModel):
191
+ role: Literal["user", "assistant"]
192
+ content: Union[str, List[ContentBlock]]
193
+
194
+
195
+ class AnthropicRequest(BaseModel):
196
+ model: str
197
+ messages: List[AnthropicMessage]
198
+ system: Optional[Union[str, List[ContentBlock]]] = None
199
+ max_tokens: int = 1024
200
+ stream: bool = False
201
+ temperature: Optional[float] = None
202
+
203
+
204
  class ModelsResponse(BaseModel):
205
  """Models list response model"""
206
  object: str = "list"
207
  data: List[Model]
208
 
209
 
210
+ # ANTHROPIC API 兼容性函数
211
+ def stream_anthropic_generator(upstream_response: requests.Response, request_id: str, requested_model: str):
212
+ """生成 Anthropic 兼容的流式响应事件"""
213
+ usage = {"input_tokens": 0, "output_tokens": 0}
214
+
215
+ start_event = {
216
+ "type": "message_start",
217
+ "message": {
218
+ "id": request_id,
219
+ "type": "message",
220
+ "role": "assistant",
221
+ "content": [],
222
+ "model": requested_model,
223
+ "stop_reason": None,
224
+ "stop_sequence": None,
225
+ "usage": usage
226
+ }
227
+ }
228
+ yield f"event: {start_event['type']}\ndata: {json.dumps(start_event['message'])}\n\n"
229
+
230
+ # 发送 content_block_start 事件
231
+ content_start_data = {
232
+ "type": "content_block_start",
233
+ "index": 0,
234
+ "content_block": {
235
+ "type": "text",
236
+ "text": ""
237
+ }
238
+ }
239
+ yield f"event: content_block_start\ndata: {json.dumps(content_start_data)}\n\n"
240
+
241
+ # 处理上游响应
242
+ for line in upstream_response.iter_lines():
243
+ if not line.startswith(b"data:"): continue
244
+ data_str = line[5:].strip()
245
+ if not data_str: continue
246
+ try:
247
+ data = json.loads(data_str.decode('utf-8'))
248
+ delta_content = data.get("data", {}).get("delta_content", "")
249
+ phase = data.get("data", {}).get("phase", "")
250
+
251
+ # 处理内容增量
252
+ if delta_content:
253
+ out_content = transform_thinking_content(delta_content) if phase == "thinking" else delta_content
254
+ if out_content:
255
+ usage["output_tokens"] += len(out_content) // 4 # 简单估算
256
+ delta_data = {
257
+ "type": "content_block_delta",
258
+ "index": 0,
259
+ "delta": {
260
+ "type": "text_delta",
261
+ "text": out_content
262
+ }
263
+ }
264
+ yield f"event: content_block_delta\ndata: {json.dumps(delta_data)}\n\n"
265
+
266
+ # 处理结束
267
+ if data.get("data", {}).get("done", False) or phase == "done":
268
+ # 发送 content_block_stop
269
+ content_stop_data = {
270
+ "type": "content_block_stop",
271
+ "index": 0
272
+ }
273
+ yield f"event: content_block_stop\ndata: {json.dumps(content_stop_data)}\n\n"
274
+
275
+ # 发送 message_delta
276
+ message_delta_data = {
277
+ "type": "message_delta",
278
+ "delta": {
279
+ "stop_reason": "end_turn",
280
+ "stop_sequence": None,
281
+ "usage": {
282
+ "input_tokens": usage["input_tokens"],
283
+ "output_tokens": usage["output_tokens"]
284
+ }
285
+ }
286
+ }
287
+ yield f"event: message_delta\ndata: {json.dumps(message_delta_data)}\n\n"
288
+
289
+ # 发送 message_stop
290
+ yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
291
+ break
292
+
293
+ except json.JSONDecodeError:
294
+ continue
295
+
296
+
297
+ def transform_thinking_content(content: str) -> str:
298
+ """Transform thinking content according to configuration"""
299
+ # Remove summary tags
300
+ content = re.sub(r'(?s)<summary>.*?</summary>', '', content)
301
+ # Clean up remaining tags
302
+ content = content.replace("</thinking>", "").replace("<Full>", "").replace("</Full>", "")
303
+ content = content.strip()
304
+
305
+ if ServerConfig.THINKING_PROCESSING == "think":
306
+ content = re.sub(r'<details[^>]*>', '<think>', content)
307
+ content = content.replace("</details>", "</think>")
308
+ elif ServerConfig.THINKING_PROCESSING == "strip":
309
+ content = re.sub(r'<details[^>]*>', '', content)
310
+ content = content.replace("</details>", "")
311
+
312
+ # Remove line prefixes
313
+ content = content.lstrip("> ")
314
+ content = content.replace("\n> ", "\n")
315
+
316
+ return content.strip()
317
+
318
+
319
  # =============================================================================
320
  # SSE Parser
321
  # =============================================================================
 
746
  return ServerConfig.BACKUP_TOKEN
747
 
748
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
  def create_openai_response_chunk(
751
  model: str,
 
1269
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
1270
 
1271
 
1272
+ # ANTHROPIC API 兼容端点
1273
+ @app.post("/v1/messages")
1274
+ async def handle_anthropic_message(
1275
+ req: AnthropicRequest,
1276
+ x_api_key: str = Header(None, alias="x-api-key"),
1277
+ authorization: str = Header(None, alias="authorization")
1278
+ ):
1279
+ """Handle Anthropic message requests"""
1280
+ debug_log("收到 Anthropic message 请求")
1281
+
1282
+ # 验证 API key
1283
+ api_key = None
1284
+ if x_api_key:
1285
+ api_key = x_api_key
1286
+ elif authorization and authorization.startswith("Bearer "):
1287
+ api_key = authorization[7:]
1288
+
1289
+ if not api_key or api_key != ServerConfig.ANTHROPIC_API_KEY:
1290
+ debug_log(f"无效的 API key: {api_key}")
1291
+ raise HTTPException(status_code=401, detail="Invalid API key")
1292
+
1293
+ debug_log(f"API key 验证通过")
1294
+ debug_log(f"请求解析成功 - 模型: {req.model}, 流式: {req.stream}, 消息数: {len(req.messages)}")
1295
+
1296
+ # 确定上游模型和功能
1297
+ upstream_model = "GLM-4.5"
1298
+ if req.model == ServerConfig.THINKING_MODEL:
1299
+ upstream_model = "GLM-4.5-Thinking"
1300
+ elif req.model == ServerConfig.SEARCH_MODEL:
1301
+ upstream_model = "GLM-4.5-Search"
1302
+
1303
+ debug_log(f"收到请求 (模型: {req.model}) -> 代理到上游 (模型: {upstream_model})")
1304
+
1305
+ # 生成 ID
1306
+ chat_id, msg_id = generate_request_ids()
1307
+
1308
+ # 转换消息格式
1309
+ openai_messages = []
1310
+ if req.system:
1311
+ # 处理两种格式的 system 内容
1312
+ if isinstance(req.system, str):
1313
+ # 字符串格式
1314
+ system_content = req.system
1315
+ else:
1316
+ # 对象数组格式
1317
+ system_content = ""
1318
+ for block in req.system:
1319
+ if block.type == "text":
1320
+ system_content += block.text
1321
+
1322
+ openai_messages.append({"role": "system", "content": system_content})
1323
+
1324
+ for msg in req.messages:
1325
+ # 处理两种格式的内容
1326
+ if isinstance(msg.content, str):
1327
+ # 字符串格式
1328
+ text_content = msg.content
1329
+ else:
1330
+ # 对象数组格式
1331
+ text_content = ""
1332
+ for block in msg.content:
1333
+ if block.type == "text":
1334
+ text_content += block.text
1335
+
1336
+ openai_messages.append({
1337
+ "role": msg.role,
1338
+ "content": text_content
1339
+ })
1340
+
1341
+ # 构建上游请求
1342
+ upstream_messages = []
1343
+ for msg in openai_messages:
1344
+ content = msg.get("content", "")
1345
+ if content is None:
1346
+ content = ""
1347
+ upstream_messages.append(Message(
1348
+ role=msg["role"],
1349
+ content=content
1350
+ ))
1351
+
1352
+ upstream_req = UpstreamRequest(
1353
+ stream=True, # 总是使用上游的流式
1354
+ chat_id=chat_id,
1355
+ id=msg_id,
1356
+ model="0727-360B-API", # 实际的上游模型 ID
1357
+ messages=upstream_messages,
1358
+ params={},
1359
+ features={"enable_thinking": True},
1360
+ background_tasks={
1361
+ "title_generation": False,
1362
+ "tags_generation": False,
1363
+ },
1364
+ mcp_servers=[],
1365
+ model_item=ModelItem(
1366
+ id="0727-360B-API",
1367
+ name="GLM-4.5",
1368
+ owned_by="openai"
1369
+ ),
1370
+ tool_servers=[],
1371
+ variables={
1372
+ "{{USER_NAME}}": "User",
1373
+ "{{USER_LOCATION}}": "Unknown",
1374
+ "{{CURRENT_DATETIME}}": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1375
+ }
1376
+ )
1377
+
1378
+ # 获取认证 token
1379
+ auth_token = get_auth_token()
1380
+
1381
+ try:
1382
+ # 调用上游 API
1383
+ headers = get_browser_headers(chat_id)
1384
+ headers["Authorization"] = f"Bearer {auth_token}"
1385
+
1386
+ response = requests.post(
1387
+ ServerConfig.API_ENDPOINT,
1388
+ json=upstream_req.model_dump(exclude_none=True),
1389
+ headers=headers,
1390
+ timeout=60.0,
1391
+ stream=True
1392
+ )
1393
+ response.raise_for_status()
1394
+ except requests.HTTPError as e:
1395
+ debug_log(f"上游 API 返回错误状态: {e.response.status_code}, 响应: {e.response.text}")
1396
+ raise HTTPException(status_code=502, detail="Upstream API error")
1397
+ except requests.RequestException as e:
1398
+ debug_log(f"请求上游 API 失败: {e}")
1399
+ raise HTTPException(status_code=502, detail=f"Failed to call upstream API: {e}")
1400
+
1401
+ request_id = f"msg_{uuid.uuid4().hex}"
1402
+
1403
+ if req.stream:
1404
+ # 流式响应
1405
+ return StreamingResponse(
1406
+ stream_anthropic_generator(response, request_id, req.model),
1407
+ media_type="text/event-stream",
1408
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
1409
+ )
1410
+ else:
1411
+ # 非流式响应
1412
+ full_content = ""
1413
+ for line in response.iter_lines():
1414
+ if not line.startswith(b"data:"): continue
1415
+ data_str = line[5:].strip()
1416
+ if not data_str: continue
1417
+ try:
1418
+ data = json.loads(data_str.decode('utf-8'))
1419
+ delta_content = data.get("data", {}).get("delta_content", "")
1420
+ phase = data.get("data", {}).get("phase", "")
1421
+ if delta_content:
1422
+ out_content = transform_thinking_content(delta_content) if phase == "thinking" else delta_content
1423
+ if out_content: full_content += out_content
1424
+ if data.get("data", {}).get("done", False) or phase == "done":
1425
+ break
1426
+ except json.JSONDecodeError:
1427
+ continue
1428
+
1429
+ return {
1430
+ "id": request_id,
1431
+ "type": "message",
1432
+ "role": "assistant",
1433
+ "model": req.model,
1434
+ "content": [{"type": "text", "text": full_content}],
1435
+ "stop_reason": "end_turn",
1436
+ "usage": {"input_tokens": 0, "output_tokens": len(full_content) // 4}
1437
+ }
1438
+
1439
+
1440
  # =============================================================================
1441
  # Main Entry Point
1442
  # =============================================================================