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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -266
app.py CHANGED
@@ -31,121 +31,107 @@ class Config:
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)
76
- keys = [k.strip() for k in response.text.splitlines() if k.strip()]
77
  else:
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")
121
  if forwarded_for:
122
  return forwarded_for.split(",")[0].strip()
123
-
124
- real_ip = request.headers.get("x-real-ip")
125
- if real_ip:
126
- return real_ip
127
-
128
  return request.client.host
129
 
130
  # IP白名单中间件
131
  @app.middleware("http")
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,16 +157,14 @@ async def admin_login(request: Request):
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}",
@@ -204,18 +188,16 @@ async def check_all_keys(password: str):
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")
@@ -224,14 +206,13 @@ async def delete_keys_batch(request: Request):
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
 
@@ -246,224 +227,126 @@ async def add_key(request: Request):
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):
299
- buffer = ""
300
- try:
301
- async for chunk in response.aiter_bytes():
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}")
318
- continue
319
- except Exception as e:
320
- print(f"Stream Error: {str(e)}")
321
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
322
-
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
412
  @app.api_route("/api/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
413
  async def proxy(path: str, request: Request):
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,
439
- params=params,
440
- headers=headers,
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
  # 启动时初始化
 
31
  WHITELIST_IPS = os.getenv("WHITELIST_IPS", "").split(",")
32
  ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "")
33
  CHUNK_SIZE = 1
 
 
34
 
35
+ # Key信息类
36
+ class KeyInfo:
37
  def __init__(self, key: str):
38
  self.key = key
39
  self.is_valid = True
40
+ self.cooling_until = None
41
+ self.last_used = None
42
+ self.error_count = 0
43
 
44
+ def to_dict(self):
 
 
 
 
 
 
 
 
 
 
 
45
  return {
46
  "key": self.key,
47
  "is_valid": self.is_valid,
48
+ "cooling_until": self.cooling_until.isoformat() if self.cooling_until else None,
49
+ "last_used": self.last_used.isoformat() if self.last_used else None,
50
+ "error_count": self.error_count,
51
+ "status": self.get_status()
52
  }
53
 
54
+ def get_status(self):
55
+ if self.cooling_until and self.cooling_until > datetime.now():
56
+ return "cooling"
57
+ if not self.is_valid:
58
+ return "invalid"
59
+ return "valid"
60
+
61
  # 全局变量
62
+ keys_info: Dict[str, KeyInfo] = {}
63
+ chat_keys = []
64
+ first_key = None
65
 
66
  # 初始化keys
67
  def init_keys():
68
+ global keys_info, chat_keys, first_key
69
  try:
70
  if Config.KEYS_URL:
71
  response = requests.get(Config.KEYS_URL)
72
+ raw_keys = [k.strip() for k in response.text.splitlines() if k.strip()]
73
  else:
74
  with open("key.txt", "r") as f:
75
+ raw_keys = [k.strip() for k in f.readlines() if k.strip()]
 
 
 
76
 
77
+ keys_info = {k: KeyInfo(k) for k in raw_keys}
78
+ chat_keys = list(raw_keys)
79
+ first_key = raw_keys[0] if raw_keys else None
80
+ print(f"Loaded {len(raw_keys)} API keys")
81
  except Exception as e:
82
  print(f"Error loading keys: {e}")
83
+ keys_info = {}
84
+ chat_keys = []
85
+ first_key = None
 
 
 
 
 
 
86
 
87
+ # 获取可用的chat key
88
+ def get_chat_key():
89
+ valid_keys = [k for k in chat_keys if is_key_available(k)]
90
+ if not valid_keys:
91
+ raise HTTPException(status_code=500, detail="No available API keys")
92
 
93
+ # 简单轮询
94
+ key = valid_keys[0]
95
+ chat_keys.append(chat_keys.pop(0))
96
+ return key
97
+
98
+ # 检查key是否可用
99
+ def is_key_available(key: str) -> bool:
100
+ info = keys_info.get(key)
101
+ if not info or not info.is_valid:
102
+ return False
103
+ if info.cooling_until and info.cooling_until > datetime.now():
104
+ return False
105
+ return True
106
+
107
+ # 设置key冷却
108
+ def set_key_cooling(key: str, days: int = 30):
109
+ if key in keys_info:
110
+ keys_info[key].cooling_until = datetime.now() + timedelta(days=days)
111
+
112
  # 获取真实IP地址
113
  def get_client_ip(request: Request) -> str:
114
  forwarded_for = request.headers.get("x-forwarded-for")
115
  if forwarded_for:
116
  return forwarded_for.split(",")[0].strip()
 
 
 
 
 
117
  return request.client.host
118
 
119
  # IP白名单中间件
120
  @app.middleware("http")
121
+ async def access_control(request: Request, call_next):
122
+ path = request.url.path
123
+
124
+ # 管理后台相关路径不检查IP
125
+ if path.startswith("/admin") or path.startswith("/api/admin") or path.startswith("/api/keys"):
126
+ return await call_next(request)
127
+
128
+ # API调用检查白名单
129
  if Config.WHITELIST_IPS and Config.WHITELIST_IPS[0]:
130
  client_ip = get_client_ip(request)
131
+ if client_ip not in Config.WHITELIST_IPS:
132
+ raise HTTPException(status_code=403, detail="IP not allowed")
 
 
 
 
 
 
 
 
 
133
 
134
  return await call_next(request)
 
135
  # 添加静态文件支持
136
  app.mount("/static", StaticFiles(directory="static"), name="static")
137
 
 
157
  except json.JSONDecodeError:
158
  raise HTTPException(status_code=400, detail="Invalid JSON")
159
 
160
+ # 获取所有keys状态
161
  @app.get("/api/keys")
162
  async def get_keys(password: str):
163
  if password != Config.ADMIN_PASSWORD:
164
  raise HTTPException(status_code=401, detail="Invalid password")
165
+ return {"keys": [info.to_dict() for info in keys_info.values()]}
 
 
166
 
167
+ # 检查key是否有效
168
  async def check_key_valid(key: str) -> bool:
169
  headers = {
170
  "Authorization": f"Bearer {key}",
 
188
  raise HTTPException(status_code=401, detail="Invalid password")
189
 
190
  results = []
191
+ for key in keys_info:
192
  is_valid = await check_key_valid(key)
193
+ keys_info[key].is_valid = is_valid
 
194
  results.append({"key": key, "valid": is_valid})
195
 
 
196
  return {"results": results}
197
 
198
  # 批量删除keys
199
  @app.post("/api/keys/delete-batch")
200
+ async def delete_batch_keys(request: Request):
201
  try:
202
  body = await request.json()
203
  password = body.get("password")
 
206
  if password != Config.ADMIN_PASSWORD:
207
  raise HTTPException(status_code=401, detail="Invalid password")
208
 
 
209
  for key in keys_to_delete:
210
+ if key in keys_info:
211
+ del keys_info[key]
212
+ if key in chat_keys:
213
+ chat_keys.remove(key)
214
 
215
+ return {"status": "success", "deleted_count": len(keys_to_delete)}
 
216
  except Exception as e:
217
  raise HTTPException(status_code=400, detail=str(e))
218
 
 
227
  if password != Config.ADMIN_PASSWORD:
228
  raise HTTPException(status_code=401, detail="Invalid password")
229
 
230
+ if key in keys_info:
231
  raise HTTPException(status_code=400, detail="Key already exists")
232
 
233
+ # 检查key有效性
234
  is_valid = await check_key_valid(key)
235
  if not is_valid:
236
  raise HTTPException(status_code=400, detail="Invalid key")
237
 
238
+ keys_info[key] = KeyInfo(key)
239
+ chat_keys.append(key)
240
+
241
  return {"status": "success", "message": "Key added successfully"}
242
  except HTTPException:
243
  raise
244
  except Exception as e:
245
  raise HTTPException(status_code=400, detail=str(e))
246
 
247
+ # 处理API请求
248
+ async def handle_api_request(url: str, headers: dict, method: str = "GET", body: dict = None, for_chat: bool = False):
249
+ max_retries = 3
250
+ current_try = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
+ while current_try < max_retries:
253
  try:
254
+ # 获取key
255
+ key = get_chat_key() if for_chat else first_key
256
+ if not key:
257
+ raise HTTPException(status_code=500, detail="No API keys available")
258
+
259
+ headers["Authorization"] = f"Bearer {key}"
 
 
 
 
 
 
260
 
261
+ async with httpx.AsyncClient() as client:
262
+ response = await client.request(
263
+ method=method,
264
+ url=url,
265
  headers=headers,
266
+ json=body
267
  )
268
 
269
+ # 检查配额不足
270
+ if response.status_code == 429 or "insufficient_quota" in response.text:
271
+ set_key_cooling(key)
272
+ current_try += 1
273
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ return response
276
+
277
  except Exception as e:
278
+ current_try += 1
279
+ if current_try == max_retries:
280
+ raise e
281
 
282
+ # 聊天完成路由
283
+ @app.post("/api/v1/chat/completions")
284
+ async def chat_completions(request: Request):
285
  try:
286
+ body = await request.json()
 
287
  headers = {
288
+ "Content-Type": "application/json",
289
+ "Accept": "text/event-stream" if body.get("stream") else "application/json"
290
  }
291
 
292
+ response = await handle_api_request(
293
+ url=f"{Config.OPENAI_API_BASE}/chat/completions",
294
+ headers=headers,
295
+ method="POST",
296
+ body=body,
297
+ for_chat=True
298
+ )
299
+
300
+ if body.get("stream"):
301
+ return StreamingResponse(
302
+ stream_generator(response),
303
+ media_type="text/event-stream"
304
  )
305
+
306
+ return Response(
307
+ content=response.text,
308
+ media_type="application/json"
309
+ )
310
+
311
  except Exception as e:
 
312
  raise HTTPException(status_code=500, detail=str(e))
313
 
314
+ # 代理其他请求
315
  @app.api_route("/api/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
316
  async def proxy(path: str, request: Request):
317
  if path == "chat/completions":
318
  return await chat_completions(request)
319
 
320
  try:
321
+ method = request.method
322
+ body = await request.json() if method in ["POST", "PUT"] else None
323
+ headers = {"Content-Type": "application/json"}
324
 
325
+ response = await handle_api_request(
326
+ url=f"{Config.OPENAI_API_BASE}/{path}",
327
+ headers=headers,
328
+ method=method,
329
+ body=body,
330
+ for_chat=False
331
+ )
332
 
333
+ return Response(
334
+ content=response.text,
335
+ status_code=response.status_code,
336
+ media_type=response.headers.get("content-type", "application/json")
337
+ )
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  except Exception as e:
 
340
  raise HTTPException(status_code=500, detail=str(e))
341
 
342
  # 健康检查路由
343
  @app.get("/api/health")
344
  async def health_check():
345
+ available_count = sum(1 for k in keys_info.values() if is_key_available(k.key))
 
346
  return {
347
  "status": "healthy",
348
+ "total_keys": len(keys_info),
349
+ "available_keys": available_count
 
350
  }
351
 
352
  # 启动时初始化