| | 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"System: {msg.content}\n\n" |
| | 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) |
| |
|