isididiidid commited on
Commit
bdde3d7
·
verified ·
1 Parent(s): 6909ee8

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +476 -0
app.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import asyncio
4
+ import uvicorn
5
+ from fastapi import FastAPI, Request, HTTPException, Header, Depends
6
+ from fastapi.responses import StreamingResponse
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, Field
9
+ from typing import List, Optional, Dict, Any, Union
10
+ import requests
11
+ from datetime import datetime
12
+ import logging
13
+ import os
14
+ from dotenv import load_dotenv
15
+
16
+ # 加载环境变量
17
+ load_dotenv()
18
+
19
+ # 配置日志
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+ logger = logging.getLogger("openai-proxy")
25
+
26
+ # 创建FastAPI应用
27
+ app = FastAPI(
28
+ title="OpenAI API Proxy",
29
+ description="将OpenAI API请求代理到DeepSider API",
30
+ version="1.0.0"
31
+ )
32
+
33
+ # 添加CORS中间件
34
+ app.add_middleware(
35
+ CORSMiddleware,
36
+ allow_origins=["*"],
37
+ allow_credentials=True,
38
+ allow_methods=["*"],
39
+ allow_headers=["*"],
40
+ )
41
+
42
+ # 配置
43
+ DEEPSIDER_API_BASE = "https://api.chargpt.ai/api/v2"
44
+ TOKEN_INDEX = 0
45
+
46
+ # 模型映射表
47
+ MODEL_MAPPING = {
48
+ "gpt-3.5-turbo": "anthropic/claude-3.5-sonnet",
49
+ "gpt-4": "anthropic/claude-3.7-sonnet",
50
+ "gpt-4o": "openai/gpt-4o",
51
+ "gpt-4-turbo": "openai/gpt-4o",
52
+ "gpt-4o-mini": "openai/gpt-4o-mini",
53
+ "claude-3-sonnet-20240229": "anthropic/claude-3.5-sonnet",
54
+ "claude-3-opus-20240229": "anthropic/claude-3.7-sonnet",
55
+ "claude-3.5-sonnet": "anthropic/claude-3.5-sonnet",
56
+ "claude-3.7-sonnet": "anthropic/claude-3.7-sonnet",
57
+ }
58
+
59
+ # 请求头
60
+ def get_headers(api_key):
61
+ global TOKEN_INDEX
62
+ # 检查是否包含多个token(用逗号分隔)
63
+ tokens = api_key.split(',')
64
+
65
+ if len(tokens) > 0:
66
+ # 轮询选择token
67
+ current_token = tokens[TOKEN_INDEX % len(tokens)]
68
+ TOKEN_INDEX = (TOKEN_INDEX + 1) % len(tokens)
69
+ else:
70
+ current_token = api_key
71
+
72
+ return {
73
+ "accept": "*/*",
74
+ "accept-encoding": "gzip, deflate, br, zstd",
75
+ "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
76
+ "content-type": "application/json",
77
+ "origin": "chrome-extension://client",
78
+ "i-lang": "zh-CN",
79
+ "i-version": "1.1.64",
80
+ "sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24"',
81
+ "sec-ch-ua-mobile": "?0",
82
+ "sec-ch-ua-platform": "Windows",
83
+ "sec-fetch-dest": "empty",
84
+ "sec-fetch-mode": "cors",
85
+ "sec-fetch-site": "cross-site",
86
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
87
+ "authorization": f"Bearer {current_token.strip()}"
88
+ }
89
+
90
+ # OpenAI API请求模型
91
+ class ChatMessage(BaseModel):
92
+ role: str
93
+ content: str
94
+ name: Optional[str] = None
95
+
96
+ class ChatCompletionRequest(BaseModel):
97
+ model: str
98
+ messages: List[ChatMessage]
99
+ temperature: Optional[float] = 1.0
100
+ top_p: Optional[float] = 1.0
101
+ n: Optional[int] = 1
102
+ stream: Optional[bool] = False
103
+ stop: Optional[Union[List[str], str]] = None
104
+ max_tokens: Optional[int] = None
105
+ presence_penalty: Optional[float] = 0
106
+ frequency_penalty: Optional[float] = 0
107
+ user: Optional[str] = None
108
+
109
+ # 账户余额查询函数
110
+ async def check_account_balance(api_key, token_index=None):
111
+ """检查账户余额信息"""
112
+ tokens = api_key.split(',')
113
+
114
+ # 如果提供了token_index并且有效,则使用指定的token
115
+ if token_index is not None and len(tokens) > token_index:
116
+ current_token = tokens[token_index].strip()
117
+ else:
118
+ # 否则使用第一个token
119
+ current_token = tokens[0].strip() if tokens else api_key
120
+
121
+ headers = {
122
+ "accept": "*/*",
123
+ "content-type": "application/json",
124
+ "authorization": f"Bearer {current_token}"
125
+ }
126
+
127
+ try:
128
+ # 获取账户余额信息
129
+ response = requests.get(
130
+ f"{DEEPSIDER_API_BASE.replace('/v2', '')}/quota/retrieve",
131
+ headers=headers
132
+ )
133
+
134
+ if response.status_code == 200:
135
+ data = response.json()
136
+ if data.get('code') == 0:
137
+ quota_list = data.get('data', {}).get('list', [])
138
+
139
+ # 解析余额信息
140
+ quota_info = {}
141
+ for item in quota_list:
142
+ item_type = item.get('type', '')
143
+ available = item.get('available', 0)
144
+
145
+ quota_info[item_type] = {
146
+ "total": item.get('total', 0),
147
+ "available": available,
148
+ "title": item.get('title', '')
149
+ }
150
+
151
+ return True, quota_info
152
+
153
+ return False, {}
154
+
155
+ except Exception as e:
156
+ logger.warning(f"检查账户余额出错:{str(e)}")
157
+ return False, {}
158
+
159
+ # 工具函数
160
+ def verify_api_key(api_key: str = Header(..., alias="Authorization")):
161
+ """验证API密钥"""
162
+ if not api_key.startswith("Bearer "):
163
+ raise HTTPException(status_code=401, detail="Invalid API key format")
164
+ return api_key.replace("Bearer ", "")
165
+
166
+ def map_openai_to_deepsider_model(model: str) -> str:
167
+ """将OpenAI模型名称映射到DeepSider模型名称"""
168
+ return MODEL_MAPPING.get(model, "anthropic/claude-3.7-sonnet")
169
+
170
+ def format_messages_for_deepsider(messages: List[ChatMessage]) -> str:
171
+ """格式化消息列表为DeepSider API所需的提示格式"""
172
+ prompt = ""
173
+ for msg in messages:
174
+ role = msg.role
175
+ # 将OpenAI的角色映射到DeepSider能理解的格式
176
+ if role == "system":
177
+ # 系统消息放在开头 作为指导
178
+ prompt = f"{msg.content}\n\n" + prompt
179
+ elif role == "user":
180
+ prompt += f"Human: {msg.content}\n\n"
181
+ elif role == "assistant":
182
+ prompt += f"Assistant: {msg.content}\n\n"
183
+ else:
184
+ # 其他角色按用户处理
185
+ prompt += f"Human ({role}): {msg.content}\n\n"
186
+
187
+ # 如果最后一个消息不是用户的 添加一个Human前缀引导模型回答
188
+ if messages and messages[-1].role != "user":
189
+ prompt += "Human: "
190
+
191
+ return prompt.strip()
192
+
193
+ async def generate_openai_response(full_response: str, request_id: str, model: str) -> Dict:
194
+ """生成符合OpenAI API响应格式的完整响应"""
195
+ timestamp = int(time.time())
196
+ return {
197
+ "id": f"chatcmpl-{request_id}",
198
+ "object": "chat.completion",
199
+ "created": timestamp,
200
+ "model": model,
201
+ "choices": [
202
+ {
203
+ "index": 0,
204
+ "message": {
205
+ "role": "assistant",
206
+ "content": full_response
207
+ },
208
+ "finish_reason": "stop"
209
+ }
210
+ ],
211
+ "usage": {
212
+ "prompt_tokens": 0, # 无法准确计算
213
+ "completion_tokens": 0, # 无法准确计算
214
+ "total_tokens": 0 # 无法准确计算
215
+ }
216
+ }
217
+
218
+ async def stream_openai_response(response, request_id: str, model: str, api_key, token_index):
219
+ """流式返回OpenAI API格式的响应"""
220
+ timestamp = int(time.time())
221
+ full_response = ""
222
+
223
+ try:
224
+ # 将DeepSider响应流转换为OpenAI流格式
225
+ for line in response.iter_lines():
226
+ if not line:
227
+ continue
228
+
229
+ if line.startswith(b'data: '):
230
+ try:
231
+ data = json.loads(line[6:].decode('utf-8'))
232
+
233
+ if data.get('code') == 202 and data.get('data', {}).get('type') == "chat":
234
+ # 获取正文内容
235
+ content = data.get('data', {}).get('content', '')
236
+ if content:
237
+ full_response += content
238
+
239
+ # 生成OpenAI格式的流式响应
240
+ chunk = {
241
+ "id": f"chatcmpl-{request_id}",
242
+ "object": "chat.completion.chunk",
243
+ "created": timestamp,
244
+ "model": model,
245
+ "choices": [
246
+ {
247
+ "index": 0,
248
+ "delta": {
249
+ "content": content
250
+ },
251
+ "finish_reason": None
252
+ }
253
+ ]
254
+ }
255
+ yield f"data: {json.dumps(chunk)}\n\n"
256
+
257
+ elif data.get('code') == 203:
258
+ # 生成完成信号
259
+ chunk = {
260
+ "id": f"chatcmpl-{request_id}",
261
+ "object": "chat.completion.chunk",
262
+ "created": timestamp,
263
+ "model": model,
264
+ "choices": [
265
+ {
266
+ "index": 0,
267
+ "delta": {},
268
+ "finish_reason": "stop"
269
+ }
270
+ ]
271
+ }
272
+ yield f"data: {json.dumps(chunk)}\n\n"
273
+ yield "data: [DONE]\n\n"
274
+
275
+ except json.JSONDecodeError:
276
+ logger.warning(f"无法解析响应: {line}")
277
+
278
+ except Exception as e:
279
+ logger.error(f"流式响应处理出错: {str(e)}")
280
+
281
+ # 尝试使用下一个Token
282
+ tokens = api_key.split(',')
283
+ if len(tokens) > 1:
284
+ logger.info(f"尝试使用下一个Token重试请求")
285
+ # 目前我们不在这里实现自动重试,只记录错误
286
+
287
+ # 返回错误信息
288
+ error_chunk = {
289
+ "id": f"chatcmpl-{request_id}",
290
+ "object": "chat.completion.chunk",
291
+ "created": timestamp,
292
+ "model": model,
293
+ "choices": [
294
+ {
295
+ "index": 0,
296
+ "delta": {
297
+ "content": f"\n\n[处理响应时出错: {str(e)}]"
298
+ },
299
+ "finish_reason": "stop"
300
+ }
301
+ ]
302
+ }
303
+ yield f"data: {json.dumps(error_chunk)}\n\n"
304
+ yield "data: [DONE]\n\n"
305
+
306
+ # 路由定义
307
+ @app.get("/")
308
+ async def root():
309
+ return {"message": "OpenAI API Proxy服务已启动 连接至DeepSider API"}
310
+
311
+ @app.get("/v1/models")
312
+ async def list_models(api_key: str = Depends(verify_api_key)):
313
+ """列出可用的模型"""
314
+ models = []
315
+ for openai_model, _ in MODEL_MAPPING.items():
316
+ models.append({
317
+ "id": openai_model,
318
+ "object": "model",
319
+ "created": int(time.time()),
320
+ "owned_by": "openai-proxy"
321
+ })
322
+
323
+ return {
324
+ "object": "list",
325
+ "data": models
326
+ }
327
+
328
+ @app.post("/v1/chat/completions")
329
+ async def create_chat_completion(
330
+ request: Request,
331
+ api_key: str = Depends(verify_api_key)
332
+ ):
333
+ """创建聊天完成API - 支持普通请求和流式请求"""
334
+ # 解析请求体
335
+ body = await request.json()
336
+ chat_request = ChatCompletionRequest(**body)
337
+
338
+ # 生成唯一请求ID
339
+ request_id = datetime.now().strftime("%Y%m%d%H%M%S") + str(time.time_ns())[-6:]
340
+
341
+ # 映射模型
342
+ deepsider_model = map_openai_to_deepsider_model(chat_request.model)
343
+
344
+ # 准备DeepSider API所需的提示
345
+ prompt = format_messages_for_deepsider(chat_request.messages)
346
+
347
+ # 准备请求体
348
+ payload = {
349
+ "model": deepsider_model,
350
+ "prompt": prompt,
351
+ "webAccess": "close", # 默认关闭网络访问
352
+ "timezone": "Asia/Shanghai"
353
+ }
354
+
355
+ # 获取请求头(包含选择的token)
356
+ headers = get_headers(api_key)
357
+ # 获取当前使用的token
358
+ tokens = api_key.split(',')
359
+ current_token_index = (TOKEN_INDEX - 1) % len(tokens) if len(tokens) > 0 else 0
360
+
361
+ try:
362
+ # 发送请求到DeepSider API
363
+ response = requests.post(
364
+ f"{DEEPSIDER_API_BASE}/chat/conversation",
365
+ headers=headers,
366
+ json=payload,
367
+ stream=True
368
+ )
369
+
370
+ # 检查响应状态
371
+ if response.status_code != 200:
372
+ error_msg = f"DeepSider API请求失败: {response.status_code}"
373
+ try:
374
+ error_data = response.json()
375
+ error_msg += f" - {error_data.get('message', '')}"
376
+ except:
377
+ error_msg += f" - {response.text}"
378
+
379
+ logger.error(error_msg)
380
+ raise HTTPException(status_code=response.status_code, detail="API请求失败")
381
+
382
+ # 处理流式或非流式响应
383
+ if chat_request.stream:
384
+ # 返回流式响应
385
+ return StreamingResponse(
386
+ stream_openai_response(response, request_id, chat_request.model, api_key, current_token_index),
387
+ media_type="text/event-stream"
388
+ )
389
+ else:
390
+ # 收集完整响应
391
+ full_response = ""
392
+ for line in response.iter_lines():
393
+ if not line:
394
+ continue
395
+
396
+ if line.startswith(b'data: '):
397
+ try:
398
+ data = json.loads(line[6:].decode('utf-8'))
399
+
400
+ if data.get('code') == 202 and data.get('data', {}).get('type') == "chat":
401
+ content = data.get('data', {}).get('content', '')
402
+ if content:
403
+ full_response += content
404
+
405
+ except json.JSONDecodeError:
406
+ pass
407
+
408
+ # 返回OpenAI格式的完整响应
409
+ return await generate_openai_response(full_response, request_id, chat_request.model)
410
+
411
+ except HTTPException:
412
+ raise
413
+ except Exception as e:
414
+ logger.exception("处理请求时出错")
415
+ raise HTTPException(status_code=500, detail=f"内部服务器错误: {str(e)}")
416
+
417
+ @app.get("/admin/balance")
418
+ async def get_account_balance(request: Request, admin_key: str = Header(None, alias="X-Admin-Key")):
419
+ """查看账户余额"""
420
+ # 简单的管理密钥检查
421
+ expected_admin_key = os.getenv("ADMIN_KEY", "admin")
422
+ if not admin_key or admin_key != expected_admin_key:
423
+ raise HTTPException(status_code=403, detail="Unauthorized")
424
+
425
+ # 从请求头中获取API密钥
426
+ auth_header = request.headers.get("Authorization", "")
427
+ if not auth_header or not auth_header.startswith("Bearer "):
428
+ raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
429
+
430
+ api_key = auth_header.replace("Bearer ", "")
431
+ tokens = api_key.split(',')
432
+
433
+ result = {}
434
+
435
+ # 获取所有token的余额信息
436
+ for i, token in enumerate(tokens):
437
+ token_display = f"token_{i+1}"
438
+ success, quota_info = await check_account_balance(api_key, i)
439
+
440
+ if success:
441
+ result[token_display] = {
442
+ "status": "success",
443
+ "quota": quota_info
444
+ }
445
+ else:
446
+ result[token_display] = {
447
+ "status": "error",
448
+ "message": "无法获取账户余额信息"
449
+ }
450
+
451
+ return result
452
+
453
+ # 错误处理器
454
+ @app.exception_handler(404)
455
+ async def not_found_handler(request, exc):
456
+ return {
457
+ "error": {
458
+ "message": f"未找到资源: {request.url.path}",
459
+ "type": "not_found_error",
460
+ "code": "not_found"
461
+ }
462
+ }, 404
463
+
464
+ # 启动事件
465
+ @app.on_event("startup")
466
+ async def startup_event():
467
+ """服务启动时初始化"""
468
+ logger.info(f"OpenAI API代理服务已启动,可以接受请求")
469
+ logger.info(f"支持多token轮询,请在Authorization头中使用英文逗号分隔多个token")
470
+
471
+ # 主程序
472
+ if __name__ == "__main__":
473
+ # 启动服务器
474
+ port = int(os.getenv("PORT", "3000"))
475
+ logger.info(f"启动OpenAI API代理服务 端口: {port}")
476
+ uvicorn.run(app, host="0.0.0.0", port=port)