diff --git a/deploy/profile-scripts/llm_client.py b/deploy/profile-scripts/llm_client.py index 20e5ea0f..86a958ee 100644 --- a/deploy/profile-scripts/llm_client.py +++ b/deploy/profile-scripts/llm_client.py @@ -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} 成功, "