group_keys_test.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """
  2. Key健康度检测路由
  3. 手动检测key是否可用,更新状态
  4. """
  5. from __future__ import annotations
  6. import asyncio
  7. import logging
  8. import time
  9. from datetime import datetime
  10. import httpx
  11. from fastapi import APIRouter, HTTPException
  12. from pydantic import BaseModel
  13. from app.db import get_pool
  14. router = APIRouter(tags=["group_keys_test"])
  15. logger = logging.getLogger(__name__)
  16. # 检测配置
  17. TEST_TIMEOUT = 10 # 超时时间(秒)
  18. MAX_CONSECUTIVE_FAILURES = 3 # 连续失败多少次后禁用
  19. class TestResult(BaseModel):
  20. success: bool
  21. status: str # active / rate_limited / failed / banned
  22. error: str | None = None
  23. latency_ms: int = 0
  24. class TestAllResult(BaseModel):
  25. total: int
  26. active: int
  27. failed: int
  28. banned: int
  29. health_rate: float
  30. async def _test_key(api_key: str, base_url: str, provider: str) -> tuple[bool, str]:
  31. """测试key是否可用"""
  32. try:
  33. async with httpx.AsyncClient(timeout=TEST_TIMEOUT) as client:
  34. # 根据provider选择不同的测试方式
  35. if provider == "openai":
  36. url = f"{base_url.rstrip('/')}/v1/chat/completions"
  37. headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
  38. data = {"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 5}
  39. response = await client.post(url, headers=headers, json=data)
  40. elif provider == "dashscope":
  41. url = f"{base_url.rstrip('/')}/compatible-mode/v1/chat/completions"
  42. headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
  43. data = {"model": "qwen-turbo", "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 5}
  44. response = await client.post(url, headers=headers, json=data)
  45. else:
  46. # 默认OpenAI兼容
  47. url = f"{base_url.rstrip('/')}/v1/chat/completions"
  48. headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
  49. data = {"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 5}
  50. response = await client.post(url, headers=headers, json=data)
  51. status_code = response.status_code
  52. if status_code in [200, 201]:
  53. return True, ""
  54. elif status_code == 429:
  55. return False, "429_rate_limited"
  56. elif status_code in [401, 403]:
  57. return False, f"{status_code}_invalid_key"
  58. elif status_code == 400:
  59. try:
  60. error_data = response.json()
  61. error_msg = error_data.get('error', {}).get('message', '')
  62. if 'model' in error_msg.lower():
  63. return True, "" # 模型名错误但key可能是好的
  64. except:
  65. pass
  66. return False, "400_bad_request"
  67. elif status_code in [500, 502, 503, 504]:
  68. return False, f"{status_code}_server_error"
  69. else:
  70. return False, f"{status_code}_unknown"
  71. except httpx.TimeoutException:
  72. return False, "timeout"
  73. except httpx.ConnectError as e:
  74. return False, f"network: {str(e)[:50]}"
  75. except Exception as e:
  76. return False, f"unknown: {str(e)[:50]}"
  77. @router.post("/group-keys/{key_id}/test")
  78. async def test_key(key_id: int):
  79. """检测单个key的健康状态"""
  80. pool = get_pool()
  81. async with pool.acquire() as conn:
  82. # 获取key信息
  83. row = await conn.fetchrow("""
  84. SELECT gk.id, gk.api_key_id, gk.group_id, gk.status, gk.is_active,
  85. ak.key_value, ak.name as key_name,
  86. mg.name as group_name
  87. FROM group_keys gk
  88. JOIN api_keys ak ON ak.id = gk.api_key_id
  89. JOIN model_groups mg ON mg.id = gk.group_id
  90. WHERE gk.id = $1
  91. """, key_id)
  92. if not row:
  93. raise HTTPException(status_code=404, detail="Key不存在")
  94. # 获取分组的provider(从模型推断)
  95. provider = "dashscope" # 默认
  96. base_url = "https://dashscope.aliyuncs.com" # DashScope默认URL
  97. # 测试key
  98. start_time = time.time()
  99. is_healthy, error_type = await _test_key(row['key_value'], base_url, provider)
  100. end_time = time.time()
  101. latency_ms = int((end_time - start_time) * 1000)
  102. # 更新状态
  103. if is_healthy:
  104. new_status = "active"
  105. await conn.execute("""
  106. UPDATE group_keys
  107. SET status = 'active', status_msg = NULL, updated_at = NOW()
  108. WHERE id = $1
  109. """, key_id)
  110. else:
  111. if error_type == "429_rate_limited":
  112. new_status = "rate_limited"
  113. else:
  114. new_status = "failed"
  115. await conn.execute("""
  116. UPDATE group_keys
  117. SET status = $1, status_msg = $2, updated_at = NOW()
  118. WHERE id = $3
  119. """, new_status, error_type, key_id)
  120. return TestResult(
  121. success=is_healthy,
  122. status=new_status,
  123. error=error_type if not is_healthy else None,
  124. latency_ms=latency_ms,
  125. )
  126. @router.post("/groups/{group_id}/test-all")
  127. async def test_all_keys(group_id: int):
  128. """批量检测分组下所有key"""
  129. pool = get_pool()
  130. async with pool.acquire() as conn:
  131. # 获取分组下所有key
  132. rows = await conn.fetch("""
  133. SELECT gk.id, gk.api_key_id, gk.status, gk.is_active,
  134. ak.key_value
  135. FROM group_keys gk
  136. JOIN api_keys ak ON ak.id = gk.api_key_id
  137. WHERE gk.group_id = $1 AND gk.is_active = TRUE
  138. """, group_id)
  139. if not rows:
  140. return TestAllResult(total=0, active=0, failed=0, banned=0, health_rate=0)
  141. total = len(rows)
  142. active = 0
  143. failed = 0
  144. banned = 0
  145. provider = "dashscope"
  146. base_url = "https://dashscope.aliyuncs.com"
  147. for row in rows:
  148. try:
  149. start_time = time.time()
  150. is_healthy, error_type = await _test_key(row['key_value'], base_url, provider)
  151. end_time = time.time()
  152. latency_ms = int((end_time - start_time) * 1000)
  153. if is_healthy:
  154. active += 1
  155. await conn.execute("""
  156. UPDATE group_keys
  157. SET status = 'active', status_msg = NULL, updated_at = NOW()
  158. WHERE id = $1
  159. """, row['id'])
  160. else:
  161. failed += 1
  162. status = "rate_limited" if error_type == "429_rate_limited" else "failed"
  163. await conn.execute("""
  164. UPDATE group_keys
  165. SET status = $1, status_msg = $2, updated_at = NOW()
  166. WHERE id = $3
  167. """, status, error_type, row['id'])
  168. except Exception as e:
  169. failed += 1
  170. await conn.execute("""
  171. UPDATE group_keys
  172. SET status = 'failed', status_msg = $1, updated_at = NOW()
  173. WHERE id = $2
  174. """, str(e)[:100], row['id'])
  175. health_rate = (active / total * 100) if total > 0 else 0
  176. return TestAllResult(
  177. total=total,
  178. active=active,
  179. failed=failed,
  180. banned=banned,
  181. health_rate=round(health_rate, 2),
  182. )