feat(llm): OCG key池——按/api/keys实时状态排序轮换(key6优先),空输出同key升pro再切下一key
This commit is contained in:
@@ -49,6 +49,75 @@ def _load_ocg_key():
|
||||
return None
|
||||
|
||||
|
||||
def _load_key_pool():
|
||||
"""从 hermes config.yaml 解析全部 OCG key,按 AgentsMeeting /api/keys 的
|
||||
实时状态排序(可用优先、用量低优先)。失败回退到单 key6。
|
||||
返回 [(key_id, api_key), ...],按优先级排序。"""
|
||||
import re
|
||||
fallback = [("key6", _load_ocg_key())] if _load_ocg_key() else []
|
||||
try:
|
||||
with open(_HERMES_CONFIG, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
# 解析 providers 下的 ocg 块: name + api_key
|
||||
blocks = re.findall(r'^\s{2}(ocg[\w\-]*):\s*\n\s*api_key:\s*(\S+)', text, re.M)
|
||||
if not blocks:
|
||||
return fallback
|
||||
# provider名 → key_id 映射(ocg-keyN→keyN, ocg-3→key3, ocg-hy3→key1, ocg-new→key2)
|
||||
name2id = {"ocg-key6": "key6", "ocg-key5": "key5", "ocg-key4": "key4",
|
||||
"ocg-3": "key3", "ocg-hy3": "key1", "ocg-new": "key2"}
|
||||
kv = {}
|
||||
for name, key in blocks:
|
||||
kid = name2id.get(name)
|
||||
if kid and kid != "key7":
|
||||
kv[kid] = key
|
||||
if not kv:
|
||||
return fallback
|
||||
# 查询 AgentsMeeting /api/keys 实时状态
|
||||
import json as _json
|
||||
status = {}
|
||||
try:
|
||||
req = urllib.request.Request("http://127.0.0.1:5803/api/keys")
|
||||
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(req, timeout=5) as r:
|
||||
data = _json.loads(r.read().decode())
|
||||
for k in data.get("keys", []):
|
||||
m_ok = k.get("monthly", {}).get("status") == "ok"
|
||||
r_ok = k.get("rolling", {}).get("status") == "ok"
|
||||
usage = k.get("monthly", {}).get("usage_percent", 99)
|
||||
status[k["key_id"]] = (m_ok and r_ok, usage)
|
||||
except Exception:
|
||||
# API 不可达时默认全可用、key6 优先
|
||||
order = ["key6", "key5", "key4", "key2", "key1", "key3"]
|
||||
return [(k, kv[k]) for k in order if k in kv]
|
||||
# 可用且用量低优先;key6 在用量相同时优先(沿用现有配置)
|
||||
scored = []
|
||||
for kid, key in kv.items():
|
||||
ok, usage = status.get(kid, (True, 50))
|
||||
if ok:
|
||||
scored.append((usage - (2 if kid == "key6" else 0), kid, key))
|
||||
scored.sort()
|
||||
pool = [(kid, key) for _, kid, key in scored]
|
||||
return pool or fallback
|
||||
except Exception as e:
|
||||
print(f" [LLM] key池加载失败,回退单key: {e}", flush=True)
|
||||
return fallback
|
||||
|
||||
|
||||
_KEY_POOL = None
|
||||
_KEY_POOL_TS = 0
|
||||
|
||||
|
||||
def _get_key_pool():
|
||||
"""key 池缓存 5 分钟。"""
|
||||
global _KEY_POOL, _KEY_POOL_TS
|
||||
import time as _t
|
||||
if not _KEY_POOL or (_t.time() - _KEY_POOL_TS) > 300:
|
||||
_KEY_POOL = _load_key_pool()
|
||||
_KEY_POOL_TS = _t.time()
|
||||
if _KEY_POOL:
|
||||
print(f" [LLM] key池: {[k for k, _ in _KEY_POOL]}", flush=True)
|
||||
return _KEY_POOL
|
||||
|
||||
|
||||
_OCG_KEY = _load_ocg_key()
|
||||
OCG_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -128,9 +197,16 @@ def call_llm(prompt, model=None, max_tokens=4096, timeout=150,
|
||||
"max_tokens": max_tokens,
|
||||
}).encode()
|
||||
|
||||
# ── 构建通道列表:key池(OCG直连,按/api/keys实时状态排序) → gateway 兜底 ──
|
||||
channels = []
|
||||
if OCG_HEADERS:
|
||||
channels.append(("ocg", OCG_URL, OCG_HEADERS))
|
||||
pool = _get_key_pool()
|
||||
for kid, key in pool:
|
||||
if key:
|
||||
channels.append((f"ocg:{kid}", OCG_URL, {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {key}",
|
||||
"User-Agent": "curl/8.5.0",
|
||||
}))
|
||||
channels.append(("gateway", GATEWAY, {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": GATEWAY_AUTH,
|
||||
@@ -149,8 +225,8 @@ def call_llm(prompt, model=None, max_tokens=4096, timeout=150,
|
||||
elapsed = time.monotonic() - t0
|
||||
if ok:
|
||||
# ── 空输出升级:flash 对部分 prompt 稳定返回空(688617 实测),
|
||||
# 换 FALLBACK_MODEL(pro) 重试一次 ──
|
||||
if not result.strip() and model_name != FALLBACK_MODEL and OCG_HEADERS:
|
||||
# 先同 key 换 FALLBACK_MODEL(pro) 重试;仍空则让外循环切下一个 key ──
|
||||
if not result.strip() and model_name != FALLBACK_MODEL and ch_name.startswith("ocg"):
|
||||
print(f" [LLM] {ch_name} {model_name} 空输出({elapsed:.1f}s),"
|
||||
f"升级 {FALLBACK_MODEL} 重试...", flush=True)
|
||||
esc_payload = json.dumps({
|
||||
@@ -158,7 +234,7 @@ def call_llm(prompt, model=None, max_tokens=4096, timeout=150,
|
||||
"max_tokens": max_tokens,
|
||||
}).encode()
|
||||
try:
|
||||
ok2, result2 = _post(OCG_URL, OCG_HEADERS, esc_payload, timeout)
|
||||
ok2, result2 = _post(ch_url, ch_headers, esc_payload, timeout)
|
||||
total_attempts += 1
|
||||
if ok2 and result2.strip():
|
||||
print(f" [LLM] 升级 {FALLBACK_MODEL} 成功, 输出{len(result2)}字", flush=True)
|
||||
@@ -168,7 +244,7 @@ def call_llm(prompt, model=None, max_tokens=4096, timeout=150,
|
||||
"elapsed": time.monotonic() - t_start,
|
||||
"attempts": total_attempts, "channel": ch_name + "+esc",
|
||||
}
|
||||
print(f" [LLM] 升级 {FALLBACK_MODEL} 仍空/失败", flush=True)
|
||||
print(f" [LLM] {ch_name} 升级 {FALLBACK_MODEL} 仍空/失败", flush=True)
|
||||
except Exception as e2:
|
||||
print(f" [LLM] 升级 {FALLBACK_MODEL} 异常: {str(e2)[:100]}", flush=True)
|
||||
print(f" [LLM] {ch_name} 尝试{attempt+1}/{retries+1} 成功, "
|
||||
|
||||
Reference in New Issue
Block a user