bobocup commited on
Commit
accf952
·
verified ·
1 Parent(s): 188c240

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -136
app.py CHANGED
@@ -5,11 +5,12 @@ from fastapi.staticfiles import StaticFiles
5
  import httpx
6
  import os
7
  import json
8
- from typing import List, Optional
9
  import requests
10
  from itertools import cycle
11
  import asyncio
12
  import time
 
13
 
14
  # 创建FastAPI应用
15
  app = FastAPI()
@@ -30,14 +31,45 @@ class Config:
30
  WHITELIST_IPS = os.getenv("WHITELIST_IPS", "").split(",")
31
  ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
32
  CHUNK_SIZE = 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  # 全局变量
35
- keys = []
36
- key_cycle = None
37
 
38
  # 初始化keys
39
  def init_keys():
40
- global keys, key_cycle
41
  try:
42
  if Config.KEYS_URL:
43
  response = requests.get(Config.KEYS_URL)
@@ -46,13 +78,43 @@ def init_keys():
46
  with open("key.txt", "r") as f:
47
  keys = [k.strip() for k in f.readlines() if k.strip()]
48
 
49
- key_cycle = cycle(keys)
 
 
 
 
 
50
  print(f"Loaded {len(keys)} API keys")
51
  except Exception as e:
52
  print(f"Error loading keys: {e}")
53
- keys = []
54
- key_cycle = cycle(keys)
 
 
 
 
 
 
 
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  # 获取真实IP地址
57
  def get_client_ip(request: Request) -> str:
58
  forwarded_for = request.headers.get("x-forwarded-for")
@@ -70,21 +132,20 @@ def get_client_ip(request: Request) -> str:
70
  async def ip_whitelist(request: Request, call_next):
71
  if Config.WHITELIST_IPS and Config.WHITELIST_IPS[0]:
72
  client_ip = get_client_ip(request)
 
 
 
 
73
  print(f"Request from IP: {client_ip}")
74
  print(f"Allowed IPs: {Config.WHITELIST_IPS}")
75
- print(f"Request headers: {dict(request.headers)}")
76
 
77
- if client_ip not in Config.WHITELIST_IPS:
78
- print(f"Access denied for IP: {client_ip}")
79
- raise HTTPException(status_code=403, detail="IP not allowed")
 
80
  return await call_next(request)
81
 
82
- # 获取下一个key
83
- def get_next_key():
84
- if not keys:
85
- raise HTTPException(status_code=500, detail="No API keys available")
86
- return next(key_cycle)
87
-
88
  # 添加静态文件支持
89
  app.mount("/static", StaticFiles(directory="static"), name="static")
90
 
@@ -110,13 +171,16 @@ async def admin_login(request: Request):
110
  except json.JSONDecodeError:
111
  raise HTTPException(status_code=400, detail="Invalid JSON")
112
 
113
- # 获取所有keys(需要密码)
114
  @app.get("/api/keys")
115
  async def get_keys(password: str):
116
  if password != Config.ADMIN_PASSWORD:
117
  raise HTTPException(status_code=401, detail="Invalid password")
118
- return {"keys": list(keys)}
119
- # 检查key是否有效
 
 
 
120
  async def check_key_valid(key: str) -> bool:
121
  headers = {
122
  "Authorization": f"Bearer {key}",
@@ -133,66 +197,102 @@ async def check_key_valid(key: str) -> bool:
133
  except:
134
  return False
135
 
136
- # 添加新key(需要密码)
137
- @app.post("/api/keys")
138
- async def add_key(key: str, password: str):
139
  if password != Config.ADMIN_PASSWORD:
140
  raise HTTPException(status_code=401, detail="Invalid password")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- if key not in keys:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # 检查key是否有效
144
  is_valid = await check_key_valid(key)
145
  if not is_valid:
146
  raise HTTPException(status_code=400, detail="Invalid key")
147
 
148
- keys.append(key)
149
- global key_cycle
150
- key_cycle = cycle(keys)
151
  return {"status": "success", "message": "Key added successfully"}
152
-
153
- raise HTTPException(status_code=400, detail="Key already exists")
 
 
154
 
155
- # 删除key(需要密码)
156
  @app.delete("/api/keys/{key}")
157
  async def delete_key(key: str, password: str):
158
  if password != Config.ADMIN_PASSWORD:
159
  raise HTTPException(status_code=401, detail="Invalid password")
160
 
161
- if key in keys:
162
- keys.remove(key)
163
- global key_cycle
164
- key_cycle = cycle(keys)
165
  return {"status": "success", "message": "Key deleted successfully"}
166
 
167
  raise HTTPException(status_code=404, detail="Key not found")
168
-
169
- # 检查key状态(需要密码)
170
- @app.get("/api/keys/check/{key}")
171
- async def check_key(key: str, password: str):
172
- if password != Config.ADMIN_PASSWORD:
173
- raise HTTPException(status_code=401, detail="Invalid password")
 
 
174
 
175
- is_valid = await check_key_valid(key)
176
- return {"valid": is_valid}
177
-
178
- # 模型列表路由
179
- @app.get("/api/v1/models")
180
- async def list_models():
181
- headers = {
182
- "Authorization": f"Bearer {get_next_key()}",
183
- "Content-Type": "application/json"
184
- }
185
-
186
- async with httpx.AsyncClient() as client:
187
- try:
188
- response = await client.get(
189
- f"{Config.OPENAI_API_BASE}/models",
190
- headers=headers
191
- )
192
- return response.json()
193
- except Exception as e:
194
- print(f"Models Error: {str(e)}")
195
- raise HTTPException(status_code=500, detail=str(e))
196
 
197
  # 流式响应生成器
198
  async def stream_generator(response):
@@ -202,19 +302,16 @@ async def stream_generator(response):
202
  chunk_str = chunk.decode('utf-8')
203
  buffer += chunk_str
204
 
205
- # 处理buffer中的完整事件
206
  while '\n\n' in buffer:
207
  event, buffer = buffer.split('\n\n', 1)
208
  if event.startswith('data: '):
209
- data = event[6:] # 移除 "data: " 前缀
210
  if data.strip() == '[DONE]':
211
  yield f"data: [DONE]\n\n"
212
  else:
213
  try:
214
- # 解析JSON并重新格式化
215
  json_data = json.loads(data)
216
  yield f"data: {json.dumps(json_data)}\n\n"
217
- # 添加小延迟使流更平滑
218
  await asyncio.sleep(0.01)
219
  except json.JSONDecodeError:
220
  print(f"JSON decode error for data: {data}")
@@ -226,62 +323,89 @@ async def stream_generator(response):
226
  # 聊天完成路由
227
  @app.post("/api/v1/chat/completions")
228
  async def chat_completions(request: Request):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  try:
230
- # 获取请求体
231
- body = await request.body()
232
- body_json = json.loads(body)
233
 
234
- # 获取headers
235
  headers = {
236
- "Authorization": f"Bearer {get_next_key()}",
237
- "Content-Type": "application/json",
238
- "Accept": "text/event-stream" if body_json.get("stream") else "application/json"
239
  }
240
 
241
- # 构建目标URL
242
- url = f"{Config.OPENAI_API_BASE}/chat/completions"
243
-
244
- print(f"Chat request to: {url}")
245
- print(f"Request from IP: {get_client_ip(request)}")
246
- print(f"Headers: {headers}")
247
- print(f"Body: {json.dumps(body_json)}")
248
-
249
- async with httpx.AsyncClient(timeout=60.0) as client:
250
- response = await client.post(
251
- url,
252
- headers=headers,
253
- json=body_json
254
  )
255
 
256
- # 检查响应状态
257
  if response.status_code != 200:
258
- print(f"Error response: {response.status_code} - {response.text}")
259
- return Response(
260
- content=response.text,
261
- status_code=response.status_code,
262
- media_type=response.headers.get("content-type", "application/json")
263
- )
264
 
265
- # 处理流式响应
266
- if body_json.get("stream"):
267
- return StreamingResponse(
268
- stream_generator(response),
269
- media_type="text/event-stream",
270
- headers={
271
- "Cache-Control": "no-cache",
272
- "Connection": "keep-alive",
273
- "Content-Type": "text/event-stream"
274
- }
275
- )
276
-
277
- # 处理普通响应
278
- return Response(
279
- content=response.text,
280
- media_type=response.headers.get("content-type", "application/json")
281
- )
282
-
283
  except Exception as e:
284
- print(f"Chat Error: {str(e)}")
285
  raise HTTPException(status_code=500, detail=str(e))
286
 
287
  # 代理其他请求到X.AI
@@ -290,27 +414,25 @@ async def proxy(path: str, request: Request):
290
  if path == "chat/completions":
291
  return await chat_completions(request)
292
 
293
- # 获取请求体
294
- body = await request.body()
295
- body_str = body.decode() if body else ""
296
-
297
- # 获取查询参数
298
- params = dict(request.query_params)
299
-
300
- # 获取并处理headers
301
- headers = dict(request.headers)
302
- headers.pop("host", None)
303
- headers["Authorization"] = f"Bearer {get_next_key()}"
304
- headers["Content-Type"] = "application/json"
305
-
306
- # 构建目标URL
307
- url = f"{Config.OPENAI_API_BASE}/{path}"
308
-
309
- print(f"Proxy request to: {url}")
310
- print(f"Request from IP: {get_client_ip(request)}")
311
-
312
- async with httpx.AsyncClient() as client:
313
- try:
314
  response = await client.request(
315
  method=request.method,
316
  url=url,
@@ -319,20 +441,30 @@ async def proxy(path: str, request: Request):
319
  content=body_str if body_str else None
320
  )
321
 
 
 
 
322
  return Response(
323
  content=response.text,
324
  status_code=response.status_code,
325
  headers=dict(response.headers)
326
  )
327
 
328
- except Exception as e:
329
- print(f"Proxy Error: {str(e)}")
330
- raise HTTPException(status_code=500, detail=str(e))
331
 
332
  # 健康检查路由
333
  @app.get("/api/health")
334
  async def health_check():
335
- return {"status": "healthy", "key_count": len(keys)}
 
 
 
 
 
 
 
336
 
337
  # 启动时初始化
338
  @app.on_event("startup")
 
5
  import httpx
6
  import os
7
  import json
8
+ from typing import List, Optional, Dict
9
  import requests
10
  from itertools import cycle
11
  import asyncio
12
  import time
13
+ from datetime import datetime, timedelta
14
 
15
  # 创建FastAPI应用
16
  app = FastAPI()
 
31
  WHITELIST_IPS = os.getenv("WHITELIST_IPS", "").split(",")
32
  ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
33
  CHUNK_SIZE = 1
34
+ MAX_RETRIES = 3 # 最大重试次数
35
+ COOLDOWN_DAYS = 30 # key冷却时间(天)
36
+
37
+ # Key状态类
38
+ class KeyStatus:
39
+ def __init__(self, key: str):
40
+ self.key = key
41
+ self.is_valid = True
42
+ self.last_check = datetime.now()
43
+ self.cooldown_until = None
44
+ self.usage_count = 0
45
+
46
+ def set_cooldown(self):
47
+ self.cooldown_until = datetime.now() + timedelta(days=Config.COOLDOWN_DAYS)
48
+ self.is_valid = False
49
+
50
+ def check_cooldown(self) -> bool:
51
+ if self.cooldown_until and datetime.now() > self.cooldown_until:
52
+ self.cooldown_until = None
53
+ self.is_valid = True
54
+ return True
55
+ return False
56
+
57
+ def to_dict(self) -> dict:
58
+ return {
59
+ "key": self.key,
60
+ "is_valid": self.is_valid,
61
+ "last_check": self.last_check.isoformat(),
62
+ "cooldown_until": self.cooldown_until.isoformat() if self.cooldown_until else None,
63
+ "usage_count": self.usage_count
64
+ }
65
 
66
  # 全局变量
67
+ key_statuses: Dict[str, KeyStatus] = {}
68
+ chat_key_cycle = None
69
 
70
  # 初始化keys
71
  def init_keys():
72
+ global key_statuses, chat_key_cycle
73
  try:
74
  if Config.KEYS_URL:
75
  response = requests.get(Config.KEYS_URL)
 
78
  with open("key.txt", "r") as f:
79
  keys = [k.strip() for k in f.readlines() if k.strip()]
80
 
81
+ # 初始化key状态
82
+ key_statuses = {k: KeyStatus(k) for k in keys if k not in key_statuses}
83
+
84
+ # 更新轮询器
85
+ update_key_cycle()
86
+
87
  print(f"Loaded {len(keys)} API keys")
88
  except Exception as e:
89
  print(f"Error loading keys: {e}")
90
+ key_statuses = {}
91
+ chat_key_cycle = None
92
+
93
+ # 更新轮询器
94
+ def update_key_cycle():
95
+ global chat_key_cycle
96
+ valid_keys = [k for k, status in key_statuses.items()
97
+ if status.is_valid and (not status.cooldown_until or status.check_cooldown())]
98
+ chat_key_cycle = cycle(valid_keys) if valid_keys else None
99
 
100
+ # 获取下一个可用的key
101
+ async def get_next_valid_key(for_chat: bool = False) -> str:
102
+ if not key_statuses:
103
+ raise HTTPException(status_code=500, detail="No API keys available")
104
+
105
+ if for_chat:
106
+ # 聊天接口使用轮询
107
+ if not chat_key_cycle:
108
+ update_key_cycle()
109
+ if not chat_key_cycle:
110
+ raise HTTPException(status_code=500, detail="No valid API keys available")
111
+ return next(chat_key_cycle)
112
+ else:
113
+ # 其他接口使用第一个有效key
114
+ valid_keys = [k for k, status in key_statuses.items() if status.is_valid]
115
+ if not valid_keys:
116
+ raise HTTPException(status_code=500, detail="No valid API keys available")
117
+ return valid_keys[0]
118
  # 获取真实IP地址
119
  def get_client_ip(request: Request) -> str:
120
  forwarded_for = request.headers.get("x-forwarded-for")
 
132
  async def ip_whitelist(request: Request, call_next):
133
  if Config.WHITELIST_IPS and Config.WHITELIST_IPS[0]:
134
  client_ip = get_client_ip(request)
135
+
136
+ # 检查是否是管理页面请求
137
+ is_admin_request = request.url.path.startswith("/admin") or request.url.path.startswith("/api/admin") or request.url.path.startswith("/api/keys")
138
+
139
  print(f"Request from IP: {client_ip}")
140
  print(f"Allowed IPs: {Config.WHITELIST_IPS}")
141
+ print(f"Is admin request: {is_admin_request}")
142
 
143
+ if is_admin_request and client_ip not in Config.WHITELIST_IPS:
144
+ print(f"Admin access denied for IP: {client_ip}")
145
+ raise HTTPException(status_code=403, detail="IP not allowed for admin access")
146
+
147
  return await call_next(request)
148
 
 
 
 
 
 
 
149
  # 添加静态文件支持
150
  app.mount("/static", StaticFiles(directory="static"), name="static")
151
 
 
171
  except json.JSONDecodeError:
172
  raise HTTPException(status_code=400, detail="Invalid JSON")
173
 
174
+ # 获取所有keys及其状态
175
  @app.get("/api/keys")
176
  async def get_keys(password: str):
177
  if password != Config.ADMIN_PASSWORD:
178
  raise HTTPException(status_code=401, detail="Invalid password")
179
+ return {
180
+ "keys": [status.to_dict() for status in key_statuses.values()]
181
+ }
182
+
183
+ # 检查单个key是否有效
184
  async def check_key_valid(key: str) -> bool:
185
  headers = {
186
  "Authorization": f"Bearer {key}",
 
197
  except:
198
  return False
199
 
200
+ # 批量检查keys
201
+ @app.post("/api/keys/check-all")
202
+ async def check_all_keys(password: str):
203
  if password != Config.ADMIN_PASSWORD:
204
  raise HTTPException(status_code=401, detail="Invalid password")
205
+
206
+ results = []
207
+ for key in list(key_statuses.keys()):
208
+ is_valid = await check_key_valid(key)
209
+ key_statuses[key].is_valid = is_valid
210
+ key_statuses[key].last_check = datetime.now()
211
+ results.append({"key": key, "valid": is_valid})
212
+
213
+ update_key_cycle()
214
+ return {"results": results}
215
+
216
+ # 批量删除keys
217
+ @app.post("/api/keys/delete-batch")
218
+ async def delete_keys_batch(request: Request):
219
+ try:
220
+ body = await request.json()
221
+ password = body.get("password")
222
+ keys_to_delete = body.get("keys", [])
223
+
224
+ if password != Config.ADMIN_PASSWORD:
225
+ raise HTTPException(status_code=401, detail="Invalid password")
226
+
227
+ deleted_keys = []
228
+ for key in keys_to_delete:
229
+ if key in key_statuses:
230
+ del key_statuses[key]
231
+ deleted_keys.append(key)
232
 
233
+ update_key_cycle()
234
+ return {"status": "success", "deleted_keys": deleted_keys}
235
+ except Exception as e:
236
+ raise HTTPException(status_code=400, detail=str(e))
237
+
238
+ # 添加新key
239
+ @app.post("/api/keys/add")
240
+ async def add_key(request: Request):
241
+ try:
242
+ body = await request.json()
243
+ password = body.get("password")
244
+ key = body.get("key")
245
+
246
+ if password != Config.ADMIN_PASSWORD:
247
+ raise HTTPException(status_code=401, detail="Invalid password")
248
+
249
+ if key in key_statuses:
250
+ raise HTTPException(status_code=400, detail="Key already exists")
251
+
252
  # 检查key是否有效
253
  is_valid = await check_key_valid(key)
254
  if not is_valid:
255
  raise HTTPException(status_code=400, detail="Invalid key")
256
 
257
+ key_statuses[key] = KeyStatus(key)
258
+ update_key_cycle()
 
259
  return {"status": "success", "message": "Key added successfully"}
260
+ except HTTPException:
261
+ raise
262
+ except Exception as e:
263
+ raise HTTPException(status_code=400, detail=str(e))
264
 
265
+ # 删除单个key
266
  @app.delete("/api/keys/{key}")
267
  async def delete_key(key: str, password: str):
268
  if password != Config.ADMIN_PASSWORD:
269
  raise HTTPException(status_code=401, detail="Invalid password")
270
 
271
+ if key in key_statuses:
272
+ del key_statuses[key]
273
+ update_key_cycle()
 
274
  return {"status": "success", "message": "Key deleted successfully"}
275
 
276
  raise HTTPException(status_code=404, detail="Key not found")
277
+ # 处理API错误响应
278
+ async def handle_api_error(response: httpx.Response, key: str) -> bool:
279
+ """
280
+ 处理API错误响应,返回是否需要重试
281
+ """
282
+ try:
283
+ error_data = response.json()
284
+ error_message = error_data.get('error', {}).get('message', '').lower()
285
 
286
+ # 检查是否是额度不足错误
287
+ if any(msg in error_message for msg in ['rate limit', 'quota exceeded', 'insufficient_quota']):
288
+ print(f"Key {key} quota exceeded, setting cooldown")
289
+ key_statuses[key].set_cooldown()
290
+ update_key_cycle()
291
+ return True
292
+
293
+ return False
294
+ except:
295
+ return False
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  # 流式响应生成器
298
  async def stream_generator(response):
 
302
  chunk_str = chunk.decode('utf-8')
303
  buffer += chunk_str
304
 
 
305
  while '\n\n' in buffer:
306
  event, buffer = buffer.split('\n\n', 1)
307
  if event.startswith('data: '):
308
+ data = event[6:]
309
  if data.strip() == '[DONE]':
310
  yield f"data: [DONE]\n\n"
311
  else:
312
  try:
 
313
  json_data = json.loads(data)
314
  yield f"data: {json.dumps(json_data)}\n\n"
 
315
  await asyncio.sleep(0.01)
316
  except json.JSONDecodeError:
317
  print(f"JSON decode error for data: {data}")
 
323
  # 聊天完成路由
324
  @app.post("/api/v1/chat/completions")
325
  async def chat_completions(request: Request):
326
+ body = await request.body()
327
+ body_json = json.loads(body)
328
+
329
+ for attempt in range(Config.MAX_RETRIES):
330
+ try:
331
+ key = await get_next_valid_key(for_chat=True)
332
+
333
+ headers = {
334
+ "Authorization": f"Bearer {key}",
335
+ "Content-Type": "application/json",
336
+ "Accept": "text/event-stream" if body_json.get("stream") else "application/json"
337
+ }
338
+
339
+ url = f"{Config.OPENAI_API_BASE}/chat/completions"
340
+
341
+ print(f"Chat request to: {url} (attempt {attempt + 1})")
342
+ print(f"Using key: {key}")
343
+
344
+ async with httpx.AsyncClient(timeout=60.0) as client:
345
+ response = await client.post(
346
+ url,
347
+ headers=headers,
348
+ json=body_json
349
+ )
350
+
351
+ if response.status_code != 200:
352
+ needs_retry = await handle_api_error(response, key)
353
+ if needs_retry and attempt < Config.MAX_RETRIES - 1:
354
+ continue
355
+
356
+ return Response(
357
+ content=response.text,
358
+ status_code=response.status_code,
359
+ media_type=response.headers.get("content-type", "application/json")
360
+ )
361
+
362
+ # 更新使用计数
363
+ key_statuses[key].usage_count += 1
364
+
365
+ if body_json.get("stream"):
366
+ return StreamingResponse(
367
+ stream_generator(response),
368
+ media_type="text/event-stream",
369
+ headers={
370
+ "Cache-Control": "no-cache",
371
+ "Connection": "keep-alive",
372
+ "Content-Type": "text/event-stream"
373
+ }
374
+ )
375
+
376
+ return Response(
377
+ content=response.text,
378
+ media_type=response.headers.get("content-type", "application/json")
379
+ )
380
+
381
+ except Exception as e:
382
+ if attempt == Config.MAX_RETRIES - 1:
383
+ print(f"Chat Error: {str(e)}")
384
+ raise HTTPException(status_code=500, detail=str(e))
385
+
386
+ # 模型列表路由
387
+ @app.get("/api/v1/models")
388
+ async def list_models():
389
  try:
390
+ key = await get_next_valid_key(for_chat=False)
 
 
391
 
 
392
  headers = {
393
+ "Authorization": f"Bearer {key}",
394
+ "Content-Type": "application/json"
 
395
  }
396
 
397
+ async with httpx.AsyncClient() as client:
398
+ response = await client.get(
399
+ f"{Config.OPENAI_API_BASE}/models",
400
+ headers=headers
 
 
 
 
 
 
 
 
 
401
  )
402
 
 
403
  if response.status_code != 200:
404
+ await handle_api_error(response, key)
 
 
 
 
 
405
 
406
+ return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  except Exception as e:
408
+ print(f"Models Error: {str(e)}")
409
  raise HTTPException(status_code=500, detail=str(e))
410
 
411
  # 代理其他请求到X.AI
 
414
  if path == "chat/completions":
415
  return await chat_completions(request)
416
 
417
+ try:
418
+ key = await get_next_valid_key(for_chat=False)
419
+
420
+ body = await request.body()
421
+ body_str = body.decode() if body else ""
422
+
423
+ params = dict(request.query_params)
424
+
425
+ headers = dict(request.headers)
426
+ headers.pop("host", None)
427
+ headers["Authorization"] = f"Bearer {key}"
428
+ headers["Content-Type"] = "application/json"
429
+
430
+ url = f"{Config.OPENAI_API_BASE}/{path}"
431
+
432
+ print(f"Proxy request to: {url}")
433
+ print(f"Using key: {key}")
434
+
435
+ async with httpx.AsyncClient() as client:
 
 
436
  response = await client.request(
437
  method=request.method,
438
  url=url,
 
441
  content=body_str if body_str else None
442
  )
443
 
444
+ if response.status_code != 200:
445
+ await handle_api_error(response, key)
446
+
447
  return Response(
448
  content=response.text,
449
  status_code=response.status_code,
450
  headers=dict(response.headers)
451
  )
452
 
453
+ except Exception as e:
454
+ print(f"Proxy Error: {str(e)}")
455
+ raise HTTPException(status_code=500, detail=str(e))
456
 
457
  # 健康检查路由
458
  @app.get("/api/health")
459
  async def health_check():
460
+ valid_keys = len([k for k, status in key_statuses.items() if status.is_valid])
461
+ total_keys = len(key_statuses)
462
+ return {
463
+ "status": "healthy",
464
+ "total_keys": total_keys,
465
+ "valid_keys": valid_keys,
466
+ "keys_in_cooldown": total_keys - valid_keys
467
+ }
468
 
469
  # 启动时初始化
470
  @app.on_event("startup")