AZILS commited on
Commit
93c4a41
·
verified ·
1 Parent(s): 3a5f4a7

Upload 8 files

Browse files
Files changed (8) hide show
  1. Dockerfile +19 -0
  2. account_manager.py +247 -0
  3. app.py +467 -0
  4. load_balancer.py +50 -0
  5. models.py +146 -0
  6. protocol_converter.py +237 -0
  7. proxy_service.py +159 -0
  8. 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