|
import json |
|
import time |
|
import asyncio |
|
import uvicorn |
|
from fastapi import FastAPI, Request, HTTPException, Header, Depends |
|
from fastapi.responses import StreamingResponse |
|
from fastapi.middleware.cors import CORSMiddleware |
|
from pydantic import BaseModel, Field |
|
from typing import List, Optional, Dict, Any, Union |
|
import requests |
|
from datetime import datetime |
|
import logging |
|
import os |
|
from dotenv import load_dotenv |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
logging.basicConfig( |
|
level=logging.INFO, |
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
|
) |
|
logger = logging.getLogger("openai-proxy") |
|
|
|
|
|
app = FastAPI( |
|
title="OpenAI API Proxy", |
|
description="将OpenAI API请求代理到DeepSider API", |
|
version="1.0.0" |
|
) |
|
|
|
|
|
app.add_middleware( |
|
CORSMiddleware, |
|
allow_origins=["*"], |
|
allow_credentials=True, |
|
allow_methods=["*"], |
|
allow_headers=["*"], |
|
) |
|
|
|
|
|
DEEPSIDER_API_BASE = "https://api.chargpt.ai/api/v2" |
|
DEEPSIDER_TOKEN = os.getenv("DEEPSIDER_TOKEN", "").split(',') |
|
TOKEN_INDEX = 0 |
|
|
|
|
|
MODEL_MAPPING = { |
|
"gpt-3.5-turbo": "anthropic/claude-3.5-sonnet", |
|
"gpt-4": "anthropic/claude-3.7-sonnet", |
|
"gpt-4o": "openai/gpt-4o", |
|
"gpt-4-turbo": "openai/gpt-4o", |
|
"gpt-4o-mini": "openai/gpt-4o-mini", |
|
"claude-3-sonnet-20240229": "anthropic/claude-3.5-sonnet", |
|
"claude-3-opus-20240229": "anthropic/claude-3.7-sonnet", |
|
"claude-3.5-sonnet": "anthropic/claude-3.5-sonnet", |
|
"claude-3.7-sonnet": "anthropic/claude-3.7-sonnet", |
|
} |
|
|
|
|
|
token_status = {} |
|
|
|
|
|
def get_headers(): |
|
global TOKEN_INDEX |
|
|
|
if len(DEEPSIDER_TOKEN) > 0: |
|
current_token = DEEPSIDER_TOKEN[TOKEN_INDEX % len(DEEPSIDER_TOKEN)] |
|
TOKEN_INDEX = (TOKEN_INDEX + 1) % len(DEEPSIDER_TOKEN) |
|
|
|
|
|
if current_token in token_status and not token_status[current_token]["active"]: |
|
|
|
for i in range(len(DEEPSIDER_TOKEN)): |
|
next_token = DEEPSIDER_TOKEN[(TOKEN_INDEX + i) % len(DEEPSIDER_TOKEN)] |
|
if next_token not in token_status or token_status[next_token]["active"]: |
|
current_token = next_token |
|
TOKEN_INDEX = (TOKEN_INDEX + i + 1) % len(DEEPSIDER_TOKEN) |
|
break |
|
else: |
|
current_token = "" |
|
|
|
return { |
|
"accept": "*/*", |
|
"accept-encoding": "gzip, deflate, br, zstd", |
|
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7", |
|
"content-type": "application/json", |
|
"origin": "chrome-extension://client", |
|
"i-lang": "zh-CN", |
|
"i-version": "1.1.64", |
|
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24"', |
|
"sec-ch-ua-mobile": "?0", |
|
"sec-ch-ua-platform": "Windows", |
|
"sec-fetch-dest": "empty", |
|
"sec-fetch-mode": "cors", |
|
"sec-fetch-site": "cross-site", |
|
"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", |
|
"authorization": f"Bearer {current_token}" |
|
} |
|
|
|
|
|
class ChatMessage(BaseModel): |
|
role: str |
|
content: str |
|
name: Optional[str] = None |
|
|
|
class ChatCompletionRequest(BaseModel): |
|
model: str |
|
messages: List[ChatMessage] |
|
temperature: Optional[float] = 1.0 |
|
top_p: Optional[float] = 1.0 |
|
n: Optional[int] = 1 |
|
stream: Optional[bool] = False |
|
stop: Optional[Union[List[str], str]] = None |
|
max_tokens: Optional[int] = None |
|
presence_penalty: Optional[float] = 0 |
|
frequency_penalty: Optional[float] = 0 |
|
user: Optional[str] = None |
|
|
|
|
|
async def initialize_token_status(): |
|
"""初始化检查所有token的状态和余额""" |
|
global token_status |
|
|
|
for token in DEEPSIDER_TOKEN: |
|
headers = { |
|
"accept": "*/*", |
|
"content-type": "application/json", |
|
"authorization": f"Bearer {token}" |
|
} |
|
|
|
try: |
|
|
|
response = requests.get( |
|
f"{DEEPSIDER_API_BASE.replace('/v2', '')}/quota/retrieve", |
|
headers=headers |
|
) |
|
|
|
active = False |
|
quota_info = {} |
|
|
|
if response.status_code == 200: |
|
data = response.json() |
|
if data.get('code') == 0: |
|
quota_list = data.get('data', {}).get('list', []) |
|
|
|
|
|
for item in quota_list: |
|
item_type = item.get('type', '') |
|
available = item.get('available', 0) |
|
|
|
if available > 0: |
|
active = True |
|
|
|
quota_info[item_type] = { |
|
"total": item.get('total', 0), |
|
"available": available, |
|
"title": item.get('title', '') |
|
} |
|
|
|
token_status[token] = { |
|
"active": active, |
|
"quota": quota_info, |
|
"last_checked": datetime.now(), |
|
"failed_count": 0 |
|
} |
|
|
|
logger.info(f"Token {token[:8]}... 状态:{'活跃' if active else '无效'}") |
|
|
|
except Exception as e: |
|
logger.warning(f"检查Token {token[:8]}... 出错:{str(e)}") |
|
token_status[token] = { |
|
"active": False, |
|
"quota": {}, |
|
"last_checked": datetime.now(), |
|
"failed_count": 0 |
|
} |
|
|
|
|
|
def verify_api_key(api_key: str = Header(..., alias="Authorization")): |
|
"""验证API密钥""" |
|
if not api_key.startswith("Bearer "): |
|
raise HTTPException(status_code=401, detail="Invalid API key format") |
|
return api_key.replace("Bearer ", "") |
|
|
|
def map_openai_to_deepsider_model(model: str) -> str: |
|
"""将OpenAI模型名称映射到DeepSider模型名称""" |
|
return MODEL_MAPPING.get(model, "anthropic/claude-3.7-sonnet") |
|
|
|
def format_messages_for_deepsider(messages: List[ChatMessage]) -> str: |
|
"""格式化消息列表为DeepSider API所需的提示格式""" |
|
prompt = "" |
|
for msg in messages: |
|
role = msg.role |
|
|
|
if role == "system": |
|
|
|
prompt = f"{msg.content}\n\n" + prompt |
|
elif role == "user": |
|
prompt += f"Human: {msg.content}\n\n" |
|
elif role == "assistant": |
|
prompt += f"Assistant: {msg.content}\n\n" |
|
else: |
|
|
|
prompt += f"Human ({role}): {msg.content}\n\n" |
|
|
|
|
|
if messages and messages[-1].role != "user": |
|
prompt += "Human: " |
|
|
|
return prompt.strip() |
|
|
|
def update_token_status(token: str, success: bool, error_message: str = None): |
|
"""更新token的状态""" |
|
global token_status |
|
|
|
if token not in token_status: |
|
token_status[token] = { |
|
"active": True, |
|
"quota": {}, |
|
"last_checked": datetime.now(), |
|
"failed_count": 0 |
|
} |
|
|
|
if not success: |
|
token_status[token]["failed_count"] += 1 |
|
|
|
|
|
if error_message and ("配额不足" in error_message or "quota" in error_message.lower()): |
|
token_status[token]["active"] = False |
|
logger.warning(f"Token {token[:8]}... 余额不足,已标记为不活跃") |
|
|
|
|
|
if token_status[token]["failed_count"] >= 5: |
|
token_status[token]["active"] = False |
|
logger.warning(f"Token {token[:8]}... 连续失败{token_status[token]['failed_count']}次,已标记为不活跃") |
|
else: |
|
|
|
token_status[token]["failed_count"] = 0 |
|
|
|
async def generate_openai_response(full_response: str, request_id: str, model: str) -> Dict: |
|
"""生成符合OpenAI API响应格式的完整响应""" |
|
timestamp = int(time.time()) |
|
return { |
|
"id": f"chatcmpl-{request_id}", |
|
"object": "chat.completion", |
|
"created": timestamp, |
|
"model": model, |
|
"choices": [ |
|
{ |
|
"index": 0, |
|
"message": { |
|
"role": "assistant", |
|
"content": full_response |
|
}, |
|
"finish_reason": "stop" |
|
} |
|
], |
|
"usage": { |
|
"prompt_tokens": 0, |
|
"completion_tokens": 0, |
|
"total_tokens": 0 |
|
} |
|
} |
|
|
|
async def stream_openai_response(response, request_id: str, model: str, token: str): |
|
"""流式返回OpenAI API格式的响应""" |
|
timestamp = int(time.time()) |
|
full_response = "" |
|
|
|
try: |
|
|
|
for line in response.iter_lines(): |
|
if not line: |
|
continue |
|
|
|
if line.startswith(b'data: '): |
|
try: |
|
data = json.loads(line[6:].decode('utf-8')) |
|
|
|
if data.get('code') == 202 and data.get('data', {}).get('type') == "chat": |
|
|
|
content = data.get('data', {}).get('content', '') |
|
if content: |
|
full_response += content |
|
|
|
|
|
chunk = { |
|
"id": f"chatcmpl-{request_id}", |
|
"object": "chat.completion.chunk", |
|
"created": timestamp, |
|
"model": model, |
|
"choices": [ |
|
{ |
|
"index": 0, |
|
"delta": { |
|
"content": content |
|
}, |
|
"finish_reason": None |
|
} |
|
] |
|
} |
|
yield f"data: {json.dumps(chunk)}\n\n" |
|
|
|
elif data.get('code') == 203: |
|
|
|
chunk = { |
|
"id": f"chatcmpl-{request_id}", |
|
"object": "chat.completion.chunk", |
|
"created": timestamp, |
|
"model": model, |
|
"choices": [ |
|
{ |
|
"index": 0, |
|
"delta": {}, |
|
"finish_reason": "stop" |
|
} |
|
] |
|
} |
|
yield f"data: {json.dumps(chunk)}\n\n" |
|
yield "data: [DONE]\n\n" |
|
|
|
except json.JSONDecodeError: |
|
logger.warning(f"无法解析响应: {line}") |
|
|
|
|
|
update_token_status(token, True) |
|
|
|
except Exception as e: |
|
logger.error(f"流式响应处理出错: {str(e)}") |
|
|
|
update_token_status(token, False, str(e)) |
|
|
|
|
|
error_chunk = { |
|
"id": f"chatcmpl-{request_id}", |
|
"object": "chat.completion.chunk", |
|
"created": timestamp, |
|
"model": model, |
|
"choices": [ |
|
{ |
|
"index": 0, |
|
"delta": { |
|
"content": f"\n\n[处理响应时出错: {str(e)}]" |
|
}, |
|
"finish_reason": "stop" |
|
} |
|
] |
|
} |
|
yield f"data: {json.dumps(error_chunk)}\n\n" |
|
yield "data: [DONE]\n\n" |
|
|
|
|
|
@app.get("/") |
|
async def root(): |
|
return {"message": "OpenAI API Proxy服务已启动 连接至DeepSider API"} |
|
|
|
@app.get("/v1/models") |
|
async def list_models(api_key: str = Depends(verify_api_key)): |
|
"""列出可用的模型""" |
|
models = [] |
|
for openai_model, _ in MODEL_MAPPING.items(): |
|
models.append({ |
|
"id": openai_model, |
|
"object": "model", |
|
"created": int(time.time()), |
|
"owned_by": "openai-proxy" |
|
}) |
|
|
|
return { |
|
"object": "list", |
|
"data": models |
|
} |
|
|
|
@app.post("/v1/chat/completions") |
|
async def create_chat_completion( |
|
request: Request, |
|
api_key: str = Depends(verify_api_key) |
|
): |
|
"""创建聊天完成API - 支持普通请求和流式请求""" |
|
|
|
body = await request.json() |
|
chat_request = ChatCompletionRequest(**body) |
|
|
|
|
|
request_id = datetime.now().strftime("%Y%m%d%H%M%S") + str(time.time_ns())[-6:] |
|
|
|
|
|
deepsider_model = map_openai_to_deepsider_model(chat_request.model) |
|
|
|
|
|
prompt = format_messages_for_deepsider(chat_request.messages) |
|
|
|
|
|
payload = { |
|
"model": deepsider_model, |
|
"prompt": prompt, |
|
"webAccess": "close", |
|
"timezone": "Asia/Shanghai" |
|
} |
|
|
|
|
|
headers = get_headers() |
|
current_token = headers["authorization"].replace("Bearer ", "") |
|
|
|
try: |
|
|
|
response = requests.post( |
|
f"{DEEPSIDER_API_BASE}/chat/conversation", |
|
headers=headers, |
|
json=payload, |
|
stream=True |
|
) |
|
|
|
|
|
if response.status_code != 200: |
|
error_msg = f"DeepSider API请求失败: {response.status_code}" |
|
try: |
|
error_data = response.json() |
|
error_msg += f" - {error_data.get('message', '')}" |
|
except: |
|
error_msg += f" - {response.text}" |
|
|
|
logger.error(error_msg) |
|
|
|
|
|
update_token_status(current_token, False, error_msg) |
|
|
|
raise HTTPException(status_code=response.status_code, detail="API请求失败") |
|
|
|
|
|
if chat_request.stream: |
|
|
|
return StreamingResponse( |
|
stream_openai_response(response, request_id, chat_request.model, current_token), |
|
media_type="text/event-stream" |
|
) |
|
else: |
|
|
|
full_response = "" |
|
for line in response.iter_lines(): |
|
if not line: |
|
continue |
|
|
|
if line.startswith(b'data: '): |
|
try: |
|
data = json.loads(line[6:].decode('utf-8')) |
|
|
|
if data.get('code') == 202 and data.get('data', {}).get('type') == "chat": |
|
content = data.get('data', {}).get('content', '') |
|
if content: |
|
full_response += content |
|
|
|
except json.JSONDecodeError: |
|
pass |
|
|
|
|
|
update_token_status(current_token, True) |
|
|
|
|
|
return await generate_openai_response(full_response, request_id, chat_request.model) |
|
|
|
except HTTPException: |
|
raise |
|
except Exception as e: |
|
logger.exception("处理请求时出错") |
|
|
|
update_token_status(current_token, False, str(e)) |
|
raise HTTPException(status_code=500, detail=f"内部服务器错误: {str(e)}") |
|
|
|
|
|
@app.get("/admin/tokens") |
|
async def get_token_status(admin_key: str = Header(None, alias="X-Admin-Key")): |
|
"""查看所有token的状态""" |
|
|
|
expected_admin_key = os.getenv("ADMIN_KEY", "admin") |
|
if not admin_key or admin_key != expected_admin_key: |
|
raise HTTPException(status_code=403, detail="Unauthorized") |
|
|
|
|
|
safe_status = {} |
|
for token, status in token_status.items(): |
|
token_display = token[:8] + "..." if len(token) > 8 else token |
|
safe_status[token_display] = status |
|
|
|
return {"tokens": safe_status, "active_tokens": sum(1 for s in token_status.values() if s["active"])} |
|
|
|
|
|
@app.post("/admin/refresh-tokens") |
|
async def refresh_token_status(admin_key: str = Header(None, alias="X-Admin-Key")): |
|
"""手动刷新所有token的状态""" |
|
|
|
expected_admin_key = os.getenv("ADMIN_KEY", "admin") |
|
if not admin_key or admin_key != expected_admin_key: |
|
raise HTTPException(status_code=403, detail="Unauthorized") |
|
|
|
await initialize_token_status() |
|
return {"message": "所有token状态已刷新", "active_tokens": sum(1 for s in token_status.values() if s["active"])} |
|
|
|
|
|
@app.get("/v1/engines") |
|
@app.get("/v1/engines/{engine_id}") |
|
async def engines_handler(): |
|
"""兼容旧的引擎API""" |
|
raise HTTPException(status_code=404, detail="引擎API已被弃用 请使用模型API") |
|
|
|
|
|
@app.exception_handler(404) |
|
async def not_found_handler(request, exc): |
|
return { |
|
"error": { |
|
"message": f"未找到资源: {request.url.path}", |
|
"type": "not_found_error", |
|
"code": "not_found" |
|
} |
|
}, 404 |
|
|
|
|
|
@app.on_event("startup") |
|
async def startup_event(): |
|
"""服务启动时初始化token状态""" |
|
if not DEEPSIDER_TOKEN or (len(DEEPSIDER_TOKEN) == 1 and DEEPSIDER_TOKEN[0] == ""): |
|
logger.warning("未设置DEEPSIDER_TOKEN环境变量 请设置后再重启服务") |
|
else: |
|
logger.info(f"初始化 {len(DEEPSIDER_TOKEN)} 个token状态...") |
|
await initialize_token_status() |
|
active_tokens = sum(1 for s in token_status.values() if s["active"]) |
|
logger.info(f"初始化完成 活跃token: {active_tokens}/{len(DEEPSIDER_TOKEN)}") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
port = int(os.getenv("PORT", "3000")) |
|
logger.info(f"启动OpenAI API代理服务 端口: {port}") |
|
uvicorn.run(app, host="0.0.0.0", port=port) |
|
|