Upload 8 files
Browse files- Dockerfile +19 -0
- account_manager.py +247 -0
- app.py +467 -0
- load_balancer.py +50 -0
- models.py +146 -0
- protocol_converter.py +237 -0
- proxy_service.py +159 -0
- requirements.txt +7 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy application
|
| 10 |
+
COPY . .
|
| 11 |
+
|
| 12 |
+
# Create data directory
|
| 13 |
+
RUN mkdir -p /app/data
|
| 14 |
+
|
| 15 |
+
# Expose port
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
# Run application
|
| 19 |
+
CMD ["python", "app.py"]
|
account_manager.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
账号池管理器 - 支持 OAuth Token 的增删改查和自动刷新
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import httpx
|
| 7 |
+
from typing import List, Optional
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
from models import Account, OAuthToken, AccountStats
|
| 10 |
+
|
| 11 |
+
# 数据文件路径 (HF Spaces 持久化目录)
|
| 12 |
+
DATA_DIR = os.environ.get("DATA_DIR", "./data")
|
| 13 |
+
ACCOUNTS_FILE = os.path.join(DATA_DIR, "accounts.json")
|
| 14 |
+
CONFIG_FILE = os.path.join(DATA_DIR, "config.json")
|
| 15 |
+
|
| 16 |
+
# Google OAuth 配置 (Antigravity 使用的 Client ID)
|
| 17 |
+
OAUTH_CLIENT_ID = os.environ.get(
|
| 18 |
+
"OAUTH_CLIENT_ID",
|
| 19 |
+
"595848968694-r5ng3t6qb9elhe1u1h1hqgq4j2r3hgvk.apps.googleusercontent.com"
|
| 20 |
+
)
|
| 21 |
+
# 默认使用 AI Studio 的公开 Client Secret
|
| 22 |
+
OAUTH_CLIENT_SECRET = os.environ.get(
|
| 23 |
+
"OAUTH_CLIENT_SECRET",
|
| 24 |
+
"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"
|
| 25 |
+
)
|
| 26 |
+
OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class AccountManager:
|
| 30 |
+
"""账号管理器 - 支持 OAuth Token"""
|
| 31 |
+
|
| 32 |
+
def __init__(self):
|
| 33 |
+
self._accounts: dict[str, Account] = {}
|
| 34 |
+
self._current_index = 0
|
| 35 |
+
self._ensure_data_dir()
|
| 36 |
+
self._load_accounts()
|
| 37 |
+
|
| 38 |
+
def _ensure_data_dir(self):
|
| 39 |
+
"""确保数据目录存在"""
|
| 40 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 41 |
+
|
| 42 |
+
def _load_accounts(self):
|
| 43 |
+
"""从文件加载账号"""
|
| 44 |
+
if os.path.exists(ACCOUNTS_FILE):
|
| 45 |
+
try:
|
| 46 |
+
with open(ACCOUNTS_FILE, "r", encoding="utf-8") as f:
|
| 47 |
+
data = json.load(f)
|
| 48 |
+
for item in data:
|
| 49 |
+
account = Account(**item)
|
| 50 |
+
self._accounts[account.id] = account
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"加载账号失败: {e}")
|
| 53 |
+
|
| 54 |
+
def _save_accounts(self):
|
| 55 |
+
"""保存账号到文件"""
|
| 56 |
+
try:
|
| 57 |
+
data = [acc.model_dump(mode="json") for acc in self._accounts.values()]
|
| 58 |
+
with open(ACCOUNTS_FILE, "w", encoding="utf-8") as f:
|
| 59 |
+
json.dump(data, f, ensure_ascii=False, indent=2, default=str)
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"保存账号失败: {e}")
|
| 62 |
+
|
| 63 |
+
def add_account(
|
| 64 |
+
self,
|
| 65 |
+
email: str,
|
| 66 |
+
access_token: str,
|
| 67 |
+
refresh_token: str,
|
| 68 |
+
expires_in: int = 3600,
|
| 69 |
+
project_id: Optional[str] = None
|
| 70 |
+
) -> Account:
|
| 71 |
+
"""添加新账号"""
|
| 72 |
+
now = int(datetime.now().timestamp())
|
| 73 |
+
token = OAuthToken(
|
| 74 |
+
access_token=access_token,
|
| 75 |
+
refresh_token=refresh_token,
|
| 76 |
+
expires_in=expires_in,
|
| 77 |
+
expiry_timestamp=now + expires_in,
|
| 78 |
+
project_id=project_id
|
| 79 |
+
)
|
| 80 |
+
account = Account(email=email, token=token)
|
| 81 |
+
self._accounts[account.id] = account
|
| 82 |
+
self._save_accounts()
|
| 83 |
+
return account
|
| 84 |
+
|
| 85 |
+
def remove_account(self, account_id: str) -> bool:
|
| 86 |
+
"""删除账号"""
|
| 87 |
+
if account_id in self._accounts:
|
| 88 |
+
del self._accounts[account_id]
|
| 89 |
+
self._save_accounts()
|
| 90 |
+
return True
|
| 91 |
+
return False
|
| 92 |
+
|
| 93 |
+
def get_account(self, account_id: str) -> Optional[Account]:
|
| 94 |
+
"""获取单个账号"""
|
| 95 |
+
return self._accounts.get(account_id)
|
| 96 |
+
|
| 97 |
+
def get_all_accounts(self) -> List[Account]:
|
| 98 |
+
"""获取所有账号"""
|
| 99 |
+
return list(self._accounts.values())
|
| 100 |
+
|
| 101 |
+
def get_available_accounts(self) -> List[Account]:
|
| 102 |
+
"""获取所有可用账号"""
|
| 103 |
+
return [acc for acc in self._accounts.values() if acc.is_available()]
|
| 104 |
+
|
| 105 |
+
async def get_next_token(self) -> Optional[Account]:
|
| 106 |
+
"""
|
| 107 |
+
获取下一个可用的 Token(轮询机制)
|
| 108 |
+
自动刷新过期的 Token
|
| 109 |
+
"""
|
| 110 |
+
available = self.get_available_accounts()
|
| 111 |
+
if not available:
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
# Round Robin
|
| 115 |
+
self._current_index = self._current_index % len(available)
|
| 116 |
+
account = available[self._current_index]
|
| 117 |
+
self._current_index += 1
|
| 118 |
+
|
| 119 |
+
# 检查并刷新过期 Token
|
| 120 |
+
if account.is_token_expired():
|
| 121 |
+
print(f"账号 {account.email} 的 token 即将过期,正在刷新...")
|
| 122 |
+
try:
|
| 123 |
+
await self._refresh_token(account)
|
| 124 |
+
except Exception as e:
|
| 125 |
+
print(f"刷新 token 失败: {e}")
|
| 126 |
+
# 继续使用可能过期的 token,让 API 返回错误
|
| 127 |
+
|
| 128 |
+
return account
|
| 129 |
+
|
| 130 |
+
async def _refresh_token(self, account: Account):
|
| 131 |
+
"""刷新 OAuth Token"""
|
| 132 |
+
async with httpx.AsyncClient() as client:
|
| 133 |
+
response = await client.post(
|
| 134 |
+
OAUTH_TOKEN_URL,
|
| 135 |
+
data={
|
| 136 |
+
"client_id": OAUTH_CLIENT_ID,
|
| 137 |
+
"client_secret": OAUTH_CLIENT_SECRET,
|
| 138 |
+
"refresh_token": account.token.refresh_token,
|
| 139 |
+
"grant_type": "refresh_token"
|
| 140 |
+
}
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
if response.status_code != 200:
|
| 144 |
+
raise Exception(f"刷新失败: {response.text}")
|
| 145 |
+
|
| 146 |
+
data = response.json()
|
| 147 |
+
now = int(datetime.now().timestamp())
|
| 148 |
+
|
| 149 |
+
account.token.access_token = data["access_token"]
|
| 150 |
+
account.token.expires_in = data.get("expires_in", 3600)
|
| 151 |
+
account.token.expiry_timestamp = now + account.token.expires_in
|
| 152 |
+
|
| 153 |
+
self._save_accounts()
|
| 154 |
+
print(f"Token 刷新成功!有效期: {account.token.expires_in} 秒")
|
| 155 |
+
|
| 156 |
+
def update_account_stats(self, account_id: str, success: bool, error: str = None):
|
| 157 |
+
"""更新账号统计信息"""
|
| 158 |
+
account = self._accounts.get(account_id)
|
| 159 |
+
if account:
|
| 160 |
+
account.total_requests += 1
|
| 161 |
+
account.last_used = datetime.now()
|
| 162 |
+
if success:
|
| 163 |
+
account.successful_requests += 1
|
| 164 |
+
account.last_error = None
|
| 165 |
+
else:
|
| 166 |
+
account.failed_requests += 1
|
| 167 |
+
account.last_error = error
|
| 168 |
+
self._save_accounts()
|
| 169 |
+
|
| 170 |
+
def set_account_cooldown(self, account_id: str, duration_seconds: int):
|
| 171 |
+
"""设置账号冷却时间"""
|
| 172 |
+
account = self._accounts.get(account_id)
|
| 173 |
+
if account:
|
| 174 |
+
account.cooldown_until = datetime.now() + timedelta(seconds=duration_seconds)
|
| 175 |
+
self._save_accounts()
|
| 176 |
+
|
| 177 |
+
def toggle_account(self, account_id: str) -> bool:
|
| 178 |
+
"""切换账号启用状态"""
|
| 179 |
+
account = self._accounts.get(account_id)
|
| 180 |
+
if account:
|
| 181 |
+
account.enabled = not account.enabled
|
| 182 |
+
self._save_accounts()
|
| 183 |
+
return account.enabled
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
def get_stats(self) -> AccountStats:
|
| 187 |
+
"""获取统计汇总"""
|
| 188 |
+
accounts = list(self._accounts.values())
|
| 189 |
+
total_requests = sum(acc.total_requests for acc in accounts)
|
| 190 |
+
successful = sum(acc.successful_requests for acc in accounts)
|
| 191 |
+
|
| 192 |
+
return AccountStats(
|
| 193 |
+
total_accounts=len(accounts),
|
| 194 |
+
available_accounts=len([a for a in accounts if a.is_available()]),
|
| 195 |
+
total_requests=total_requests,
|
| 196 |
+
success_rate=successful / total_requests if total_requests > 0 else 0.0
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class ConfigManager:
|
| 201 |
+
"""配置管理器 - 管理 API Key 等可变配置"""
|
| 202 |
+
|
| 203 |
+
def __init__(self):
|
| 204 |
+
self._config = {
|
| 205 |
+
"api_key": "sk-antigravity"
|
| 206 |
+
}
|
| 207 |
+
self._ensure_data_dir()
|
| 208 |
+
self._load_config()
|
| 209 |
+
|
| 210 |
+
def _ensure_data_dir(self):
|
| 211 |
+
"""确保数据目录存在"""
|
| 212 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 213 |
+
|
| 214 |
+
def _load_config(self):
|
| 215 |
+
"""从文件加载配置"""
|
| 216 |
+
if os.path.exists(CONFIG_FILE):
|
| 217 |
+
try:
|
| 218 |
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
| 219 |
+
self._config.update(json.load(f))
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"加载配置失败: {e}")
|
| 222 |
+
|
| 223 |
+
def _save_config(self):
|
| 224 |
+
"""保存配置到文件"""
|
| 225 |
+
try:
|
| 226 |
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
| 227 |
+
json.dump(self._config, f, ensure_ascii=False, indent=2)
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f"保存配置失败: {e}")
|
| 230 |
+
|
| 231 |
+
def get_api_key(self) -> str:
|
| 232 |
+
"""获取 API Key"""
|
| 233 |
+
return self._config.get("api_key", "sk-antigravity")
|
| 234 |
+
|
| 235 |
+
def set_api_key(self, api_key: str) -> bool:
|
| 236 |
+
"""设置 API Key"""
|
| 237 |
+
if not api_key or len(api_key.strip()) == 0:
|
| 238 |
+
return False
|
| 239 |
+
self._config["api_key"] = api_key.strip()
|
| 240 |
+
self._save_config()
|
| 241 |
+
return True
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# 全局单例
|
| 245 |
+
account_manager = AccountManager()
|
| 246 |
+
config_manager = ConfigManager()
|
| 247 |
+
|
app.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API 反代服务 - 主应用程序
|
| 3 |
+
Gradio 管理界面 + FastAPI API 端点
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from fastapi import FastAPI, Request, HTTPException
|
| 9 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
| 10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from contextlib import asynccontextmanager
|
| 12 |
+
|
| 13 |
+
from models import OpenAIChatRequest, OpenAIMessage, SUPPORTED_MODELS, ServiceConfig
|
| 14 |
+
from account_manager import account_manager, config_manager
|
| 15 |
+
from load_balancer import load_balancer
|
| 16 |
+
from proxy_service import stream_chat_completion, chat_completion
|
| 17 |
+
|
| 18 |
+
# ============ 服务配置 ============
|
| 19 |
+
config = ServiceConfig()
|
| 20 |
+
|
| 21 |
+
# 从环境变量获取管理员凭据
|
| 22 |
+
ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "admin")
|
| 23 |
+
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "antigravity")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ============ FastAPI 应用 ============
|
| 27 |
+
@asynccontextmanager
|
| 28 |
+
async def lifespan(app: FastAPI):
|
| 29 |
+
"""应用生命周期管理"""
|
| 30 |
+
print("🚀 API 反代服务启动中...")
|
| 31 |
+
yield
|
| 32 |
+
print("👋 API 反代服务关闭")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
app = FastAPI(
|
| 36 |
+
title="Antigravity API Proxy",
|
| 37 |
+
description="OpenAI 兼容的 API 反代服务",
|
| 38 |
+
version="1.0.0",
|
| 39 |
+
lifespan=lifespan
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# CORS 中间件
|
| 43 |
+
app.add_middleware(
|
| 44 |
+
CORSMiddleware,
|
| 45 |
+
allow_origins=["*"],
|
| 46 |
+
allow_credentials=True,
|
| 47 |
+
allow_methods=["*"],
|
| 48 |
+
allow_headers=["*"],
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ============ API 端点 ============
|
| 53 |
+
|
| 54 |
+
@app.get("/v1/models")
|
| 55 |
+
async def list_models():
|
| 56 |
+
"""列出支持的模型"""
|
| 57 |
+
return {
|
| 58 |
+
"object": "list",
|
| 59 |
+
"data": [
|
| 60 |
+
{
|
| 61 |
+
"id": model["id"],
|
| 62 |
+
"object": "model",
|
| 63 |
+
"owned_by": "antigravity",
|
| 64 |
+
"permission": []
|
| 65 |
+
}
|
| 66 |
+
for model in SUPPORTED_MODELS
|
| 67 |
+
]
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.post("/v1/chat/completions")
|
| 72 |
+
async def chat_completions(request: Request):
|
| 73 |
+
"""
|
| 74 |
+
OpenAI 兼容的 Chat Completion API
|
| 75 |
+
"""
|
| 76 |
+
# 验证 API Key
|
| 77 |
+
auth_header = request.headers.get("Authorization", "")
|
| 78 |
+
if not auth_header.startswith("Bearer "):
|
| 79 |
+
raise HTTPException(status_code=401, detail="Missing API key")
|
| 80 |
+
|
| 81 |
+
# 验证 API Key 是否匹配
|
| 82 |
+
provided_key = auth_header[7:] # 去掉 "Bearer " 前缀
|
| 83 |
+
expected_key = config_manager.get_api_key()
|
| 84 |
+
if provided_key != expected_key:
|
| 85 |
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
| 86 |
+
|
| 87 |
+
# 获取请求体
|
| 88 |
+
body = await request.json()
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
openai_request = OpenAIChatRequest(
|
| 92 |
+
model=body.get("model", "gemini-2.5-flash"),
|
| 93 |
+
messages=[OpenAIMessage(**msg) for msg in body.get("messages", [])],
|
| 94 |
+
temperature=body.get("temperature", 1.0),
|
| 95 |
+
top_p=body.get("top_p", 0.95),
|
| 96 |
+
max_tokens=body.get("max_tokens", 8192),
|
| 97 |
+
stream=body.get("stream", False)
|
| 98 |
+
)
|
| 99 |
+
except Exception as e:
|
| 100 |
+
raise HTTPException(status_code=400, detail=f"Invalid request: {e}")
|
| 101 |
+
|
| 102 |
+
# 获取可用账号
|
| 103 |
+
account = await load_balancer.get_next_account()
|
| 104 |
+
if not account:
|
| 105 |
+
raise HTTPException(status_code=503, detail="No available accounts")
|
| 106 |
+
|
| 107 |
+
# 处理请求
|
| 108 |
+
if openai_request.stream:
|
| 109 |
+
return StreamingResponse(
|
| 110 |
+
stream_chat_completion(openai_request, account),
|
| 111 |
+
media_type="text/event-stream",
|
| 112 |
+
headers={
|
| 113 |
+
"Cache-Control": "no-cache",
|
| 114 |
+
"Connection": "keep-alive",
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
result = await chat_completion(openai_request, account)
|
| 119 |
+
if "error" in result:
|
| 120 |
+
raise HTTPException(status_code=500, detail=result["error"])
|
| 121 |
+
return JSONResponse(content=result)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@app.get("/health")
|
| 125 |
+
async def health_check():
|
| 126 |
+
"""健康检查"""
|
| 127 |
+
stats = account_manager.get_stats()
|
| 128 |
+
return {
|
| 129 |
+
"status": "healthy",
|
| 130 |
+
"accounts": stats.total_accounts,
|
| 131 |
+
"available": stats.available_accounts
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@app.post("/admin/import-accounts")
|
| 136 |
+
async def import_accounts(request: Request):
|
| 137 |
+
"""
|
| 138 |
+
批量导入账号 (需要管理员密码)
|
| 139 |
+
请求体格式:
|
| 140 |
+
{
|
| 141 |
+
"password": "admin_password",
|
| 142 |
+
"accounts": [
|
| 143 |
+
{"email": "xxx@gmail.com", "refresh_token": "1//xxx..."},
|
| 144 |
+
...
|
| 145 |
+
]
|
| 146 |
+
}
|
| 147 |
+
"""
|
| 148 |
+
body = await request.json()
|
| 149 |
+
|
| 150 |
+
# 验证管理员密码
|
| 151 |
+
if body.get("password") != ADMIN_PASSWORD:
|
| 152 |
+
raise HTTPException(status_code=401, detail="Invalid admin password")
|
| 153 |
+
|
| 154 |
+
accounts_data = body.get("accounts", [])
|
| 155 |
+
if not accounts_data:
|
| 156 |
+
raise HTTPException(status_code=400, detail="No accounts provided")
|
| 157 |
+
|
| 158 |
+
results = []
|
| 159 |
+
for acc in accounts_data:
|
| 160 |
+
email = acc.get("email")
|
| 161 |
+
refresh_token = acc.get("refresh_token")
|
| 162 |
+
|
| 163 |
+
if not email or not refresh_token:
|
| 164 |
+
results.append({"email": email, "status": "error", "message": "Missing email or refresh_token"})
|
| 165 |
+
continue
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
account = account_manager.add_account(
|
| 169 |
+
email=email,
|
| 170 |
+
access_token="pending", # 将在首次使用时自动刷新
|
| 171 |
+
refresh_token=refresh_token,
|
| 172 |
+
expires_in=0 # 立即过期,强制刷新
|
| 173 |
+
)
|
| 174 |
+
results.append({"email": email, "status": "success", "id": account.id})
|
| 175 |
+
except Exception as e:
|
| 176 |
+
results.append({"email": email, "status": "error", "message": str(e)})
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
"imported": len([r for r in results if r["status"] == "success"]),
|
| 180 |
+
"failed": len([r for r in results if r["status"] == "error"]),
|
| 181 |
+
"results": results
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# ============ Gradio 管理界面 ============
|
| 186 |
+
|
| 187 |
+
def get_accounts_table():
|
| 188 |
+
"""获取账号列表表格数据"""
|
| 189 |
+
accounts = account_manager.get_all_accounts()
|
| 190 |
+
if not accounts:
|
| 191 |
+
return [["暂无账号", "-", "-", "-", "-", "-"]]
|
| 192 |
+
|
| 193 |
+
return [
|
| 194 |
+
[
|
| 195 |
+
acc.id,
|
| 196 |
+
acc.email,
|
| 197 |
+
"✅ 正常" if acc.is_available() else "❌ 冷却中",
|
| 198 |
+
str(acc.total_requests),
|
| 199 |
+
f"{acc.successful_requests / acc.total_requests * 100:.1f}%" if acc.total_requests > 0 else "-",
|
| 200 |
+
acc.last_used.strftime("%Y-%m-%d %H:%M") if acc.last_used else "-"
|
| 201 |
+
]
|
| 202 |
+
for acc in accounts
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def add_account(email: str, access_token: str, refresh_token: str, project_id: str):
|
| 207 |
+
"""添加账号"""
|
| 208 |
+
if not email or not access_token or not refresh_token:
|
| 209 |
+
return "❌ 请填写完整信息", get_accounts_table()
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
account = account_manager.add_account(
|
| 213 |
+
email=email,
|
| 214 |
+
access_token=access_token,
|
| 215 |
+
refresh_token=refresh_token,
|
| 216 |
+
project_id=project_id if project_id else None
|
| 217 |
+
)
|
| 218 |
+
return f"✅ 账号 {email} 添加成功!", get_accounts_table()
|
| 219 |
+
except Exception as e:
|
| 220 |
+
return f"❌ 添加失败: {e}", get_accounts_table()
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def delete_account(email_to_delete: str):
|
| 224 |
+
"""删除账号"""
|
| 225 |
+
if not email_to_delete or email_to_delete == "暂无账号":
|
| 226 |
+
return "❌ 请选择要删除的账号", get_accounts_table()
|
| 227 |
+
|
| 228 |
+
# 查找账号 ID
|
| 229 |
+
accounts = account_manager.get_all_accounts()
|
| 230 |
+
target_account = None
|
| 231 |
+
for acc in accounts:
|
| 232 |
+
if acc.email == email_to_delete:
|
| 233 |
+
target_account = acc
|
| 234 |
+
break
|
| 235 |
+
|
| 236 |
+
if not target_account:
|
| 237 |
+
return f"❌ 账号 {email_to_delete} 不存在", get_accounts_table()
|
| 238 |
+
|
| 239 |
+
try:
|
| 240 |
+
if account_manager.remove_account(target_account.id):
|
| 241 |
+
return f"✅ 账号 {email_to_delete} 已删除", get_accounts_table()
|
| 242 |
+
else:
|
| 243 |
+
return f"❌ 删除账号 {email_to_delete} 失败", get_accounts_table()
|
| 244 |
+
except Exception as e:
|
| 245 |
+
return f"❌ 删除失败: {e}", get_accounts_table()
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def get_account_emails():
|
| 249 |
+
"""获取所有账号邮箱列表 (用于更新 Dropdown)"""
|
| 250 |
+
accounts = account_manager.get_all_accounts()
|
| 251 |
+
if not accounts:
|
| 252 |
+
return gr.update(choices=["暂无账号"], value=None)
|
| 253 |
+
emails = [acc.email for acc in accounts]
|
| 254 |
+
return gr.update(choices=emails, value=None)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def _get_account_emails_list():
|
| 258 |
+
"""获取账号邮箱列表 (用于初始化)"""
|
| 259 |
+
accounts = account_manager.get_all_accounts()
|
| 260 |
+
if not accounts:
|
| 261 |
+
return ["暂无账号"]
|
| 262 |
+
return [acc.email for acc in accounts]
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def update_api_key(new_api_key: str):
|
| 266 |
+
"""更新 API Key"""
|
| 267 |
+
if not new_api_key or len(new_api_key.strip()) < 3:
|
| 268 |
+
return "❌ API Key 不能为空且至少需要 3 个字符", config_manager.get_api_key()
|
| 269 |
+
|
| 270 |
+
if config_manager.set_api_key(new_api_key.strip()):
|
| 271 |
+
return f"✅ API Key 已更新为: {new_api_key.strip()}", new_api_key.strip()
|
| 272 |
+
else:
|
| 273 |
+
return "❌ 更新 API Key 失败", config_manager.get_api_key()
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def get_service_info():
|
| 277 |
+
"""获取服务信息"""
|
| 278 |
+
stats = account_manager.get_stats()
|
| 279 |
+
current_api_key = config_manager.get_api_key()
|
| 280 |
+
|
| 281 |
+
# 获取当前 Space URL
|
| 282 |
+
space_url = os.environ.get("SPACE_HOST", "localhost:7860")
|
| 283 |
+
if not space_url.startswith("http"):
|
| 284 |
+
space_url = f"https://{space_url}"
|
| 285 |
+
|
| 286 |
+
return f"""
|
| 287 |
+
## 🔗 API 配置
|
| 288 |
+
|
| 289 |
+
**Base URL:** `{space_url}/v1`
|
| 290 |
+
|
| 291 |
+
**API Key:** `{current_api_key}`
|
| 292 |
+
|
| 293 |
+
**支持的模型:**
|
| 294 |
+
- `gemini-2.5-pro` / `gemini-2.5-flash` / `gemini-2.5-flash-lite`
|
| 295 |
+
- `gemini-3-flash` / `gemini-3-pro` / `gemini-3-pro-high`
|
| 296 |
+
- `claude-sonnet-4-5-thinking` / `claude-sonnet-4-5` / `claude-opus-4-5`
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
## 📊 服务状态
|
| 301 |
+
|
| 302 |
+
| 指标 | 值 |
|
| 303 |
+
|------|-----|
|
| 304 |
+
| 总账号数 | {stats.total_accounts} |
|
| 305 |
+
| 可用账号 | {stats.available_accounts} |
|
| 306 |
+
| 总请求数 | {stats.total_requests} |
|
| 307 |
+
| 成功率 | {stats.success_rate * 100:.1f}% |
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
## 🛠️ 使用方法
|
| 312 |
+
|
| 313 |
+
### cURL 示例
|
| 314 |
+
```bash
|
| 315 |
+
curl {space_url}/v1/chat/completions \\
|
| 316 |
+
-H "Content-Type: application/json" \\
|
| 317 |
+
-H "Authorization: Bearer {current_api_key}" \\
|
| 318 |
+
-d '{{
|
| 319 |
+
"model": "gemini-2.5-flash",
|
| 320 |
+
"messages": [{{"role": "user", "content": "Hello"}}],
|
| 321 |
+
"stream": true
|
| 322 |
+
}}'
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
### NextChat / Cherry Studio 配置
|
| 326 |
+
1. API 类型: OpenAI
|
| 327 |
+
2. Base URL: `{space_url}/v1`
|
| 328 |
+
3. API Key: `{current_api_key}`
|
| 329 |
+
"""
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# 创建 Gradio 界面
|
| 333 |
+
with gr.Blocks(
|
| 334 |
+
title="Antigravity API Proxy",
|
| 335 |
+
theme=gr.themes.Soft(
|
| 336 |
+
primary_hue="purple",
|
| 337 |
+
secondary_hue="blue",
|
| 338 |
+
),
|
| 339 |
+
css="""
|
| 340 |
+
.container { max-width: 1200px; margin: auto; }
|
| 341 |
+
.logo { font-size: 2em; font-weight: bold; }
|
| 342 |
+
.danger-btn { background-color: #dc3545 !important; }
|
| 343 |
+
"""
|
| 344 |
+
) as demo:
|
| 345 |
+
gr.Markdown(
|
| 346 |
+
"""
|
| 347 |
+
# 🚀 Antigravity API Proxy
|
| 348 |
+
### 将 Gemini/Claude 转换为 OpenAI 兼容 API
|
| 349 |
+
""",
|
| 350 |
+
elem_classes="logo"
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
with gr.Tabs():
|
| 354 |
+
# 仪表盘
|
| 355 |
+
with gr.Tab("📊 仪表盘"):
|
| 356 |
+
service_info = gr.Markdown(get_service_info)
|
| 357 |
+
refresh_btn = gr.Button("🔄 刷新状态")
|
| 358 |
+
refresh_btn.click(fn=get_service_info, outputs=service_info)
|
| 359 |
+
|
| 360 |
+
# 账号管理
|
| 361 |
+
with gr.Tab("👤 账号管理"):
|
| 362 |
+
gr.Markdown("### 添加新账号")
|
| 363 |
+
gr.Markdown("> 需要从 Antigravity 桌面应用导出账号信息,或手动获取 OAuth Token")
|
| 364 |
+
|
| 365 |
+
with gr.Row():
|
| 366 |
+
email_input = gr.Textbox(label="邮箱", placeholder="example@gmail.com")
|
| 367 |
+
project_id_input = gr.Textbox(label="Project ID (选填)", placeholder="可留空")
|
| 368 |
+
|
| 369 |
+
access_token_input = gr.Textbox(
|
| 370 |
+
label="Access Token",
|
| 371 |
+
placeholder="ya29.xxx... 或填 pending",
|
| 372 |
+
lines=2
|
| 373 |
+
)
|
| 374 |
+
refresh_token_input = gr.Textbox(
|
| 375 |
+
label="Refresh Token",
|
| 376 |
+
placeholder="1//xxx...",
|
| 377 |
+
lines=2
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
add_btn = gr.Button("➕ 添加账号", variant="primary")
|
| 381 |
+
result_text = gr.Textbox(label="结果", interactive=False)
|
| 382 |
+
|
| 383 |
+
gr.Markdown("---")
|
| 384 |
+
gr.Markdown("### 账号列表")
|
| 385 |
+
accounts_table = gr.Dataframe(
|
| 386 |
+
headers=["ID", "邮箱", "状态", "请求数", "成功率", "最后使用"],
|
| 387 |
+
value=get_accounts_table,
|
| 388 |
+
interactive=False
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
gr.Markdown("### 删除账号")
|
| 392 |
+
with gr.Row():
|
| 393 |
+
delete_email_dropdown = gr.Dropdown(
|
| 394 |
+
label="选择要删除的账号",
|
| 395 |
+
choices=_get_account_emails_list(),
|
| 396 |
+
interactive=True
|
| 397 |
+
)
|
| 398 |
+
delete_btn = gr.Button("🗑️ 删除账号", variant="stop")
|
| 399 |
+
delete_result = gr.Textbox(label="删除结果", interactive=False)
|
| 400 |
+
|
| 401 |
+
# 绑定事件
|
| 402 |
+
add_btn.click(
|
| 403 |
+
fn=add_account,
|
| 404 |
+
inputs=[email_input, access_token_input, refresh_token_input, project_id_input],
|
| 405 |
+
outputs=[result_text, accounts_table]
|
| 406 |
+
).then(
|
| 407 |
+
fn=get_account_emails,
|
| 408 |
+
outputs=delete_email_dropdown
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
delete_btn.click(
|
| 412 |
+
fn=delete_account,
|
| 413 |
+
inputs=[delete_email_dropdown],
|
| 414 |
+
outputs=[delete_result, accounts_table]
|
| 415 |
+
).then(
|
| 416 |
+
fn=get_account_emails,
|
| 417 |
+
outputs=delete_email_dropdown
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
# 设置
|
| 421 |
+
with gr.Tab("⚙️ 设置"):
|
| 422 |
+
gr.Markdown("### API Key 配置")
|
| 423 |
+
gr.Markdown("> 修改后,所有 API 请求需要使用新的 API Key")
|
| 424 |
+
|
| 425 |
+
with gr.Row():
|
| 426 |
+
api_key_input = gr.Textbox(
|
| 427 |
+
label="当前 API Key",
|
| 428 |
+
value=config_manager.get_api_key,
|
| 429 |
+
placeholder="输入新的 API Key",
|
| 430 |
+
interactive=True
|
| 431 |
+
)
|
| 432 |
+
save_api_key_btn = gr.Button("💾 保存 API Key", variant="primary")
|
| 433 |
+
|
| 434 |
+
api_key_result = gr.Textbox(label="结果", interactive=False)
|
| 435 |
+
|
| 436 |
+
save_api_key_btn.click(
|
| 437 |
+
fn=update_api_key,
|
| 438 |
+
inputs=[api_key_input],
|
| 439 |
+
outputs=[api_key_result, api_key_input]
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
gr.Markdown("---")
|
| 443 |
+
gr.Markdown("### 环境变量配置")
|
| 444 |
+
gr.Markdown("""
|
| 445 |
+
> 以下配置在 HF Spaces 环境下通过 Secrets 设置
|
| 446 |
+
|
| 447 |
+
| 配置项 | 环境变量 | 默认值 |
|
| 448 |
+
|--------|----------|--------|
|
| 449 |
+
| 数据目录 | `DATA_DIR` | `./data` |
|
| 450 |
+
| 管理员用户名 | `ADMIN_USERNAME` | `admin` |
|
| 451 |
+
| 管理员密码 | `ADMIN_PASSWORD` | `antigravity` |
|
| 452 |
+
""")
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
# 挂载 Gradio 到 FastAPI (带密码保护)
|
| 456 |
+
app = gr.mount_gradio_app(
|
| 457 |
+
app,
|
| 458 |
+
demo,
|
| 459 |
+
path="/",
|
| 460 |
+
auth=(ADMIN_USERNAME, ADMIN_PASSWORD),
|
| 461 |
+
auth_message="🔐 请输入管理员凭据登录 Antigravity API Proxy"
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
if __name__ == "__main__":
|
| 466 |
+
import uvicorn
|
| 467 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
load_balancer.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
负载均衡器 - Round Robin + 自动故障转移
|
| 3 |
+
"""
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from account_manager import account_manager
|
| 7 |
+
from models import Account
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class LoadBalancer:
|
| 11 |
+
"""智能负载均衡器"""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.cooldown_duration = 60 # 默认冷却时间(秒)
|
| 15 |
+
self.max_retries = 3
|
| 16 |
+
|
| 17 |
+
async def get_next_account(self) -> Optional[Account]:
|
| 18 |
+
"""获取下一个可用账号"""
|
| 19 |
+
return await account_manager.get_next_token()
|
| 20 |
+
|
| 21 |
+
def mark_account_error(self, account_id: str, error_code: int, error_msg: str = ""):
|
| 22 |
+
"""
|
| 23 |
+
标记账号错误,根据错误类型决定是否冷却
|
| 24 |
+
- 429: Too Many Requests - 立即冷却
|
| 25 |
+
- 400: Bad Request - 可能是请求格式问题,不冷却
|
| 26 |
+
- 401: Unauthorized - Token 失效,冷却
|
| 27 |
+
- 403: Forbidden - 配额耗尽,长时间冷却
|
| 28 |
+
"""
|
| 29 |
+
account_manager.update_account_stats(account_id, success=False, error=error_msg)
|
| 30 |
+
|
| 31 |
+
if error_code == 429:
|
| 32 |
+
# 速率限制,冷却 60 秒
|
| 33 |
+
account_manager.set_account_cooldown(account_id, 60)
|
| 34 |
+
print(f"账号 {account_id} 触发速率限制,冷却 60 秒")
|
| 35 |
+
elif error_code == 401:
|
| 36 |
+
# Token 失效,冷却 30 秒(等待刷新)
|
| 37 |
+
account_manager.set_account_cooldown(account_id, 30)
|
| 38 |
+
print(f"账号 {account_id} Token 失效,冷却 30 秒")
|
| 39 |
+
elif error_code == 403:
|
| 40 |
+
# 配额耗尽,冷却 5 分钟
|
| 41 |
+
account_manager.set_account_cooldown(account_id, 300)
|
| 42 |
+
print(f"账号 {account_id} 配额耗尽,冷却 5 分钟")
|
| 43 |
+
|
| 44 |
+
def mark_account_success(self, account_id: str):
|
| 45 |
+
"""标记请求成功"""
|
| 46 |
+
account_manager.update_account_stats(account_id, success=True)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# 全局单例
|
| 50 |
+
load_balancer = LoadBalancer()
|
models.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
账号数据模型定义 - 支持 Web Session Token (OAuth) 方式
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Optional, Literal, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class OAuthToken(BaseModel):
|
| 11 |
+
"""OAuth Token 信息"""
|
| 12 |
+
access_token: str
|
| 13 |
+
refresh_token: str
|
| 14 |
+
expires_in: int = 3600
|
| 15 |
+
expiry_timestamp: int = 0 # Unix 时间戳
|
| 16 |
+
project_id: Optional[str] = None
|
| 17 |
+
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Account(BaseModel):
|
| 21 |
+
"""API 账号模型 - Web Session Token 方式"""
|
| 22 |
+
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
| 23 |
+
email: str
|
| 24 |
+
token: OAuthToken
|
| 25 |
+
enabled: bool = True
|
| 26 |
+
created_at: datetime = Field(default_factory=datetime.now)
|
| 27 |
+
|
| 28 |
+
# 统计信息
|
| 29 |
+
total_requests: int = 0
|
| 30 |
+
successful_requests: int = 0
|
| 31 |
+
failed_requests: int = 0
|
| 32 |
+
last_used: Optional[datetime] = None
|
| 33 |
+
last_error: Optional[str] = None
|
| 34 |
+
|
| 35 |
+
# 冷却状态
|
| 36 |
+
cooldown_until: Optional[datetime] = None
|
| 37 |
+
|
| 38 |
+
def is_available(self) -> bool:
|
| 39 |
+
"""检查账号是否可用"""
|
| 40 |
+
if not self.enabled:
|
| 41 |
+
return False
|
| 42 |
+
if self.cooldown_until and datetime.now() < self.cooldown_until:
|
| 43 |
+
return False
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
def is_token_expired(self) -> bool:
|
| 47 |
+
"""检查 token 是否过期(提前5分钟刷新)"""
|
| 48 |
+
now = int(datetime.now().timestamp())
|
| 49 |
+
return now >= self.token.expiry_timestamp - 300
|
| 50 |
+
|
| 51 |
+
def display_name(self) -> str:
|
| 52 |
+
"""显示名称"""
|
| 53 |
+
return self.email
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class AccountStats(BaseModel):
|
| 57 |
+
"""账号统计汇总"""
|
| 58 |
+
total_accounts: int = 0
|
| 59 |
+
available_accounts: int = 0
|
| 60 |
+
total_requests: int = 0
|
| 61 |
+
success_rate: float = 0.0
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ServiceConfig(BaseModel):
|
| 65 |
+
"""服务配置"""
|
| 66 |
+
api_key: str = "sk-antigravity"
|
| 67 |
+
admin_username: str = "admin"
|
| 68 |
+
admin_password: str = "antigravity"
|
| 69 |
+
port: int = 7860
|
| 70 |
+
enable_cors: bool = True
|
| 71 |
+
max_retries: int = 3
|
| 72 |
+
retry_delay: float = 1.0
|
| 73 |
+
cooldown_duration: int = 60 # 秒
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ============ OpenAI 兼容格式 ============
|
| 77 |
+
|
| 78 |
+
class OpenAIMessage(BaseModel):
|
| 79 |
+
"""OpenAI 消息格式"""
|
| 80 |
+
role: Literal["system", "user", "assistant"]
|
| 81 |
+
content: str
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class OpenAIChatRequest(BaseModel):
|
| 85 |
+
"""OpenAI Chat Completion 请求"""
|
| 86 |
+
model: str
|
| 87 |
+
messages: list[OpenAIMessage]
|
| 88 |
+
temperature: Optional[float] = 1.0
|
| 89 |
+
top_p: Optional[float] = 0.95
|
| 90 |
+
max_tokens: Optional[int] = 8192
|
| 91 |
+
stream: bool = False
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class OpenAIChatChoice(BaseModel):
|
| 95 |
+
"""OpenAI 响应选项"""
|
| 96 |
+
index: int = 0
|
| 97 |
+
message: Optional[Dict[str, Any]] = None
|
| 98 |
+
delta: Optional[Dict[str, Any]] = None
|
| 99 |
+
finish_reason: Optional[str] = None
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class OpenAIChatResponse(BaseModel):
|
| 103 |
+
"""OpenAI Chat Completion 响应"""
|
| 104 |
+
id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex[:8]}")
|
| 105 |
+
object: str = "chat.completion"
|
| 106 |
+
created: int = Field(default_factory=lambda: int(datetime.now().timestamp()))
|
| 107 |
+
model: str
|
| 108 |
+
choices: list[OpenAIChatChoice]
|
| 109 |
+
usage: Optional[Dict[str, int]] = None
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ============ 模型映射 ============
|
| 113 |
+
|
| 114 |
+
MODEL_MAPPING: Dict[str, str] = {
|
| 115 |
+
# Claude 到 Gemini 映射
|
| 116 |
+
"claude-sonnet-4-5-thinking": "gemini-2.5-pro-preview",
|
| 117 |
+
"claude-sonnet-4-5": "gemini-2.5-flash-preview",
|
| 118 |
+
"claude-opus-4-5": "gemini-2.5-pro-preview",
|
| 119 |
+
"claude-3-5-sonnet": "gemini-2.5-flash-preview",
|
| 120 |
+
"claude-3-5-haiku": "gemini-2.5-flash-lite-preview",
|
| 121 |
+
|
| 122 |
+
# Gemini 模型直通
|
| 123 |
+
"gemini-3-flash": "gemini-3-flash-preview",
|
| 124 |
+
"gemini-3-pro": "gemini-3-pro-preview",
|
| 125 |
+
"gemini-3-pro-high": "gemini-3-pro-preview",
|
| 126 |
+
"gemini-2.5-pro": "gemini-2.5-pro-preview",
|
| 127 |
+
"gemini-2.5-flash": "gemini-2.5-flash-preview",
|
| 128 |
+
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite-preview",
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
SUPPORTED_MODELS = [
|
| 132 |
+
# Gemini 3 系列
|
| 133 |
+
{"id": "gemini-3-flash", "name": "Gemini 3 Flash (极速预览)"},
|
| 134 |
+
{"id": "gemini-3-pro", "name": "Gemini 3 Pro (经典版)"},
|
| 135 |
+
{"id": "gemini-3-pro-high", "name": "Gemini 3 Pro (最强推理)"},
|
| 136 |
+
|
| 137 |
+
# Gemini 2.5 系列
|
| 138 |
+
{"id": "gemini-2.5-pro", "name": "Gemini 2.5 Pro (极致推理)"},
|
| 139 |
+
{"id": "gemini-2.5-flash", "name": "Gemini 2.5 Flash (极速响应)"},
|
| 140 |
+
{"id": "gemini-2.5-flash-lite", "name": "Gemini 2.5 Flash Lite (轻量极速)"},
|
| 141 |
+
|
| 142 |
+
# Claude 映射
|
| 143 |
+
{"id": "claude-sonnet-4-5-thinking", "name": "Claude Sonnet 4.5 - 思维链"},
|
| 144 |
+
{"id": "claude-sonnet-4-5", "name": "Claude Sonnet 4.5"},
|
| 145 |
+
{"id": "claude-opus-4-5", "name": "Claude Opus 4.5"},
|
| 146 |
+
]
|
protocol_converter.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
协议转换器 - OpenAI 格式 <-> Gemini 格式
|
| 3 |
+
"""
|
| 4 |
+
from typing import Dict, Any, List
|
| 5 |
+
from models import OpenAIChatRequest, MODEL_MAPPING
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def convert_openai_to_gemini(request: OpenAIChatRequest) -> Dict[str, Any]:
|
| 9 |
+
"""
|
| 10 |
+
将 OpenAI Chat Completion 请求转换为 Gemini 格式
|
| 11 |
+
|
| 12 |
+
OpenAI 格式:
|
| 13 |
+
{
|
| 14 |
+
"model": "gpt-4",
|
| 15 |
+
"messages": [
|
| 16 |
+
{"role": "system", "content": "You are..."},
|
| 17 |
+
{"role": "user", "content": "Hello"}
|
| 18 |
+
]
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
Gemini 格式:
|
| 22 |
+
{
|
| 23 |
+
"contents": [{"role": "user", "parts": [{"text": "Hello"}]}],
|
| 24 |
+
"systemInstruction": {"role": "user", "parts": [{"text": "You are..."}]},
|
| 25 |
+
"generationConfig": {...}
|
| 26 |
+
}
|
| 27 |
+
"""
|
| 28 |
+
contents = []
|
| 29 |
+
system_instruction = None
|
| 30 |
+
|
| 31 |
+
for msg in request.messages:
|
| 32 |
+
if msg.role == "system":
|
| 33 |
+
system_instruction = {
|
| 34 |
+
"role": "user",
|
| 35 |
+
"parts": [{"text": msg.content}]
|
| 36 |
+
}
|
| 37 |
+
elif msg.role == "user":
|
| 38 |
+
contents.append({
|
| 39 |
+
"role": "user",
|
| 40 |
+
"parts": [{"text": msg.content}]
|
| 41 |
+
})
|
| 42 |
+
elif msg.role == "assistant":
|
| 43 |
+
contents.append({
|
| 44 |
+
"role": "model",
|
| 45 |
+
"parts": [{"text": msg.content}]
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
# 如果没有 system instruction,使用空字符串
|
| 49 |
+
if system_instruction is None:
|
| 50 |
+
system_instruction = {"role": "user", "parts": [{"text": ""}]}
|
| 51 |
+
|
| 52 |
+
# Generation Config
|
| 53 |
+
generation_config = {
|
| 54 |
+
"temperature": request.temperature or 1.0,
|
| 55 |
+
"topP": request.top_p or 0.95,
|
| 56 |
+
"maxOutputTokens": request.max_tokens or 8192,
|
| 57 |
+
"candidateCount": 1,
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
# 检查是否需要启用思维链(thinking)
|
| 61 |
+
model_lower = request.model.lower()
|
| 62 |
+
if "thinking" in model_lower or "sonnet-3-7" in model_lower:
|
| 63 |
+
generation_config["thinkingConfig"] = {
|
| 64 |
+
"includeThoughts": True,
|
| 65 |
+
"thinkingBudget": 8191, # Google Protocol Limit < 8192
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return {
|
| 69 |
+
"contents": contents,
|
| 70 |
+
"systemInstruction": system_instruction,
|
| 71 |
+
"generationConfig": generation_config,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def map_model_name(model: str) -> str:
|
| 76 |
+
"""
|
| 77 |
+
映射模型名称到 Gemini API 支持的名称
|
| 78 |
+
|
| 79 |
+
支持灵活匹配:
|
| 80 |
+
- 精确匹配: claude-sonnet-4-5 -> gemini-2.5-flash-preview
|
| 81 |
+
- 模糊匹配: 包含 opus -> gemini-2.5-pro-preview
|
| 82 |
+
"""
|
| 83 |
+
# 先尝试精确匹配
|
| 84 |
+
if model in MODEL_MAPPING:
|
| 85 |
+
return MODEL_MAPPING[model]
|
| 86 |
+
|
| 87 |
+
# 模糊匹配
|
| 88 |
+
model_lower = model.lower()
|
| 89 |
+
|
| 90 |
+
# Gemini 模型直通(添加 -preview 后缀如需要)
|
| 91 |
+
if model_lower.startswith("gemini-"):
|
| 92 |
+
if not model_lower.endswith("-preview"):
|
| 93 |
+
# 某些模型需要 -preview 后缀
|
| 94 |
+
if model_lower in ["gemini-3-flash", "gemini-3-pro", "gemini-2.5-pro", "gemini-2.5-flash"]:
|
| 95 |
+
return model + "-preview"
|
| 96 |
+
return model
|
| 97 |
+
|
| 98 |
+
# Claude 模型映射
|
| 99 |
+
if "opus" in model_lower:
|
| 100 |
+
return "gemini-2.5-pro-preview"
|
| 101 |
+
if "sonnet" in model_lower:
|
| 102 |
+
if "thinking" in model_lower:
|
| 103 |
+
return "gemini-2.5-pro-preview"
|
| 104 |
+
return "gemini-2.5-flash-preview"
|
| 105 |
+
if "haiku" in model_lower:
|
| 106 |
+
return "gemini-2.5-flash-lite-preview"
|
| 107 |
+
|
| 108 |
+
# 默认返回原模型名
|
| 109 |
+
return model
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def convert_gemini_to_openai_chunk(gemini_data: Dict[str, Any], model: str) -> Dict[str, Any]:
|
| 113 |
+
"""
|
| 114 |
+
将 Gemini 流式响应转换为 OpenAI chunk 格式
|
| 115 |
+
|
| 116 |
+
Gemini 格式:
|
| 117 |
+
{
|
| 118 |
+
"candidates": [{
|
| 119 |
+
"content": {"parts": [{"text": "Hello"}]},
|
| 120 |
+
"finishReason": "STOP"
|
| 121 |
+
}]
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
OpenAI 格式:
|
| 125 |
+
{
|
| 126 |
+
"id": "chatcmpl-xxx",
|
| 127 |
+
"object": "chat.completion.chunk",
|
| 128 |
+
"choices": [{
|
| 129 |
+
"index": 0,
|
| 130 |
+
"delta": {"content": "Hello"},
|
| 131 |
+
"finish_reason": null
|
| 132 |
+
}]
|
| 133 |
+
}
|
| 134 |
+
"""
|
| 135 |
+
import uuid
|
| 136 |
+
from datetime import datetime
|
| 137 |
+
|
| 138 |
+
# 解析 Gemini 响应
|
| 139 |
+
candidates = gemini_data.get("candidates", [])
|
| 140 |
+
if not candidates:
|
| 141 |
+
# 可能是嵌套在 response 中
|
| 142 |
+
response = gemini_data.get("response", {})
|
| 143 |
+
candidates = response.get("candidates", [])
|
| 144 |
+
|
| 145 |
+
text = ""
|
| 146 |
+
finish_reason = None
|
| 147 |
+
is_thought = False
|
| 148 |
+
thought_signature = None
|
| 149 |
+
|
| 150 |
+
if candidates:
|
| 151 |
+
candidate = candidates[0]
|
| 152 |
+
content = candidate.get("content", {})
|
| 153 |
+
parts = content.get("parts", [])
|
| 154 |
+
|
| 155 |
+
if parts:
|
| 156 |
+
part = parts[0]
|
| 157 |
+
text = part.get("text", "")
|
| 158 |
+
is_thought = part.get("thought", False)
|
| 159 |
+
thought_signature = part.get("thoughtSignature")
|
| 160 |
+
|
| 161 |
+
# 转换结束原因
|
| 162 |
+
gemini_reason = candidate.get("finishReason")
|
| 163 |
+
if gemini_reason == "STOP":
|
| 164 |
+
finish_reason = "stop"
|
| 165 |
+
elif gemini_reason == "MAX_TOKENS":
|
| 166 |
+
finish_reason = "length"
|
| 167 |
+
elif gemini_reason == "SAFETY":
|
| 168 |
+
finish_reason = "content_filter"
|
| 169 |
+
|
| 170 |
+
# 构建 OpenAI chunk
|
| 171 |
+
delta = {"content": text}
|
| 172 |
+
if is_thought:
|
| 173 |
+
delta["thought"] = True
|
| 174 |
+
if thought_signature:
|
| 175 |
+
delta["thoughtSignature"] = thought_signature
|
| 176 |
+
|
| 177 |
+
return {
|
| 178 |
+
"id": gemini_data.get("responseId", f"chatcmpl-{uuid.uuid4().hex[:8]}"),
|
| 179 |
+
"object": "chat.completion.chunk",
|
| 180 |
+
"created": int(datetime.now().timestamp()),
|
| 181 |
+
"model": model,
|
| 182 |
+
"choices": [{
|
| 183 |
+
"index": 0,
|
| 184 |
+
"delta": delta,
|
| 185 |
+
"finish_reason": finish_reason
|
| 186 |
+
}]
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def convert_gemini_to_openai_response(gemini_data: Dict[str, Any], model: str) -> Dict[str, Any]:
|
| 191 |
+
"""
|
| 192 |
+
将 Gemini 非流式响应转换为 OpenAI 格式
|
| 193 |
+
"""
|
| 194 |
+
import uuid
|
| 195 |
+
from datetime import datetime
|
| 196 |
+
|
| 197 |
+
candidates = gemini_data.get("candidates", [])
|
| 198 |
+
if not candidates:
|
| 199 |
+
response = gemini_data.get("response", {})
|
| 200 |
+
candidates = response.get("candidates", [])
|
| 201 |
+
|
| 202 |
+
text = ""
|
| 203 |
+
finish_reason = "stop"
|
| 204 |
+
|
| 205 |
+
if candidates:
|
| 206 |
+
candidate = candidates[0]
|
| 207 |
+
content = candidate.get("content", {})
|
| 208 |
+
parts = content.get("parts", [])
|
| 209 |
+
|
| 210 |
+
if parts:
|
| 211 |
+
text = parts[0].get("text", "")
|
| 212 |
+
|
| 213 |
+
gemini_reason = candidate.get("finishReason")
|
| 214 |
+
if gemini_reason == "MAX_TOKENS":
|
| 215 |
+
finish_reason = "length"
|
| 216 |
+
elif gemini_reason == "SAFETY":
|
| 217 |
+
finish_reason = "content_filter"
|
| 218 |
+
|
| 219 |
+
return {
|
| 220 |
+
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 221 |
+
"object": "chat.completion",
|
| 222 |
+
"created": int(datetime.now().timestamp()),
|
| 223 |
+
"model": model,
|
| 224 |
+
"choices": [{
|
| 225 |
+
"index": 0,
|
| 226 |
+
"message": {
|
| 227 |
+
"role": "assistant",
|
| 228 |
+
"content": text
|
| 229 |
+
},
|
| 230 |
+
"finish_reason": finish_reason
|
| 231 |
+
}],
|
| 232 |
+
"usage": {
|
| 233 |
+
"prompt_tokens": 0,
|
| 234 |
+
"completion_tokens": 0,
|
| 235 |
+
"total_tokens": 0
|
| 236 |
+
}
|
| 237 |
+
}
|
proxy_service.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API 代理服务 - 调用 Google Antigravity API
|
| 3 |
+
"""
|
| 4 |
+
import httpx
|
| 5 |
+
import json
|
| 6 |
+
import uuid
|
| 7 |
+
from typing import AsyncGenerator
|
| 8 |
+
from models import OpenAIChatRequest, Account
|
| 9 |
+
from protocol_converter import convert_openai_to_gemini, map_model_name, convert_gemini_to_openai_chunk
|
| 10 |
+
from load_balancer import load_balancer
|
| 11 |
+
|
| 12 |
+
# Google Antigravity 内部 API 端点
|
| 13 |
+
GEMINI_API_URL = "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def stream_chat_completion(
|
| 17 |
+
request: OpenAIChatRequest,
|
| 18 |
+
account: Account
|
| 19 |
+
) -> AsyncGenerator[str, None]:
|
| 20 |
+
"""
|
| 21 |
+
流式 Chat Completion
|
| 22 |
+
|
| 23 |
+
1. 将 OpenAI 请求转换为 Gemini 格式
|
| 24 |
+
2. 调用 Google Antigravity API
|
| 25 |
+
3. 将 Gemini 响应转换回 OpenAI 格式
|
| 26 |
+
"""
|
| 27 |
+
# 转换请求
|
| 28 |
+
gemini_request = convert_openai_to_gemini(request)
|
| 29 |
+
|
| 30 |
+
# 映射模型名
|
| 31 |
+
upstream_model = map_model_name(request.model)
|
| 32 |
+
|
| 33 |
+
# 构建请求体
|
| 34 |
+
request_body = {
|
| 35 |
+
"project": account.token.project_id or "default-project",
|
| 36 |
+
"requestId": str(uuid.uuid4()),
|
| 37 |
+
"model": upstream_model,
|
| 38 |
+
"userAgent": "antigravity-hf",
|
| 39 |
+
"request": {
|
| 40 |
+
"contents": gemini_request["contents"],
|
| 41 |
+
"systemInstruction": gemini_request["systemInstruction"],
|
| 42 |
+
"generationConfig": gemini_request["generationConfig"],
|
| 43 |
+
"sessionId": account.token.session_id
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# 发送请求
|
| 48 |
+
url = f"{GEMINI_API_URL}?alt=sse"
|
| 49 |
+
|
| 50 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 51 |
+
async with client.stream(
|
| 52 |
+
"POST",
|
| 53 |
+
url,
|
| 54 |
+
json=request_body,
|
| 55 |
+
headers={
|
| 56 |
+
"Authorization": f"Bearer {account.token.access_token}",
|
| 57 |
+
"Host": "daily-cloudcode-pa.sandbox.googleapis.com",
|
| 58 |
+
"User-Agent": "antigravity-hf/1.0",
|
| 59 |
+
"Content-Type": "application/json"
|
| 60 |
+
}
|
| 61 |
+
) as response:
|
| 62 |
+
if response.status_code != 200:
|
| 63 |
+
error_text = await response.aread()
|
| 64 |
+
error_msg = f"上游服务错误 ({response.status_code}): {error_text.decode()}"
|
| 65 |
+
|
| 66 |
+
# 标记错误
|
| 67 |
+
load_balancer.mark_account_error(
|
| 68 |
+
account.id,
|
| 69 |
+
response.status_code,
|
| 70 |
+
error_msg
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# 返回错误
|
| 74 |
+
yield f"data: {json.dumps({'error': error_msg})}\n\n"
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
# 处理 SSE 流
|
| 78 |
+
buffer = ""
|
| 79 |
+
async for chunk in response.aiter_text():
|
| 80 |
+
buffer += chunk
|
| 81 |
+
|
| 82 |
+
# 按行解析 SSE 事件
|
| 83 |
+
while "\n" in buffer:
|
| 84 |
+
line, buffer = buffer.split("\n", 1)
|
| 85 |
+
line = line.strip()
|
| 86 |
+
|
| 87 |
+
if not line:
|
| 88 |
+
continue
|
| 89 |
+
|
| 90 |
+
if line.startswith("data: "):
|
| 91 |
+
data = line[6:]
|
| 92 |
+
|
| 93 |
+
if data == "[DONE]":
|
| 94 |
+
yield "data: [DONE]\n\n"
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
gemini_data = json.loads(data)
|
| 99 |
+
openai_chunk = convert_gemini_to_openai_chunk(
|
| 100 |
+
gemini_data,
|
| 101 |
+
request.model
|
| 102 |
+
)
|
| 103 |
+
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
| 104 |
+
except json.JSONDecodeError:
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# 标记成功
|
| 108 |
+
load_balancer.mark_account_success(account.id)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
async def chat_completion(
|
| 112 |
+
request: OpenAIChatRequest,
|
| 113 |
+
account: Account
|
| 114 |
+
) -> dict:
|
| 115 |
+
"""
|
| 116 |
+
非流式 Chat Completion
|
| 117 |
+
"""
|
| 118 |
+
from protocol_converter import convert_gemini_to_openai_response
|
| 119 |
+
|
| 120 |
+
# 转换请求
|
| 121 |
+
gemini_request = convert_openai_to_gemini(request)
|
| 122 |
+
upstream_model = map_model_name(request.model)
|
| 123 |
+
|
| 124 |
+
request_body = {
|
| 125 |
+
"project": account.token.project_id or "default-project",
|
| 126 |
+
"requestId": str(uuid.uuid4()),
|
| 127 |
+
"model": upstream_model,
|
| 128 |
+
"userAgent": "antigravity-hf",
|
| 129 |
+
"request": {
|
| 130 |
+
"contents": gemini_request["contents"],
|
| 131 |
+
"systemInstruction": gemini_request["systemInstruction"],
|
| 132 |
+
"generationConfig": gemini_request["generationConfig"],
|
| 133 |
+
"sessionId": account.token.session_id
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
# 非流式 URL
|
| 138 |
+
url = "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent"
|
| 139 |
+
|
| 140 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 141 |
+
response = await client.post(
|
| 142 |
+
url,
|
| 143 |
+
json=request_body,
|
| 144 |
+
headers={
|
| 145 |
+
"Authorization": f"Bearer {account.token.access_token}",
|
| 146 |
+
"Host": "daily-cloudcode-pa.sandbox.googleapis.com",
|
| 147 |
+
"User-Agent": "antigravity-hf/1.0",
|
| 148 |
+
"Content-Type": "application/json"
|
| 149 |
+
}
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
if response.status_code != 200:
|
| 153 |
+
error_msg = f"上游服务错误 ({response.status_code}): {response.text}"
|
| 154 |
+
load_balancer.mark_account_error(account.id, response.status_code, error_msg)
|
| 155 |
+
return {"error": error_msg}
|
| 156 |
+
|
| 157 |
+
load_balancer.mark_account_success(account.id)
|
| 158 |
+
gemini_data = response.json()
|
| 159 |
+
return convert_gemini_to_openai_response(gemini_data, request.model)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==4.44.0
|
| 2 |
+
huggingface_hub==0.25.0
|
| 3 |
+
fastapi>=0.100.0
|
| 4 |
+
uvicorn>=0.23.0
|
| 5 |
+
httpx>=0.25.0
|
| 6 |
+
pydantic>=2.0.0
|
| 7 |
+
python-dotenv>=1.0.0
|