#!/usr/bin/env python3 """Revive disabled-but-live channels from reconcile.json by DELETE + RECREATE. The NewAPI fork's PUT cannot change `status` (rejected as "Invalid parameters") and the batch endpoint DELETES by default, so the reliable way to revive a channel is to delete it and recreate with the correct config. Pulls the real key from the vault (matched by name suffix) and sets test_model to the model that direct verification found working. """ import json, re, sys, time from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed sys.path.insert(0, str(Path(__file__).resolve().parent)) import newapi_client as n import importlib.util spec = importlib.util.spec_from_file_location( "r", str(Path(__file__).resolve().parent / "reconcile_newapi.py")) R = importlib.util.module_from_spec(spec); spec.loader.exec_module(R) H = Path(__file__).resolve().parent / "llm-key-hunter" report = json.loads((H / "results/reconcile.json").read_text()) chans = {c["id"]: c for c in json.loads(Path("/tmp/chans.json").read_text())} all_keys = R.load_all_keys() def match_key(name, prov): """Find vault/bucket key matching a channel name suffix (last4 or last6).""" suf = name.rsplit("_", 1)[-1].lower().split("-")[0] for k in all_keys: if k["prov"] != prov: continue kl = k["key"].lower() if len(suf) == 4 and kl[-4:] == suf: return k["key"] if len(suf) >= 5 and kl[-len(suf):] == suf: return k["key"] # fallback: last6 ends with suf for k in all_keys: if k["prov"] == prov and k["key"].lower()[-6:].endswith(suf): return k["key"] return None def zhipu_channel(name, key, kind, model): if kind == "coding": return dict(name=name, type=n.TYPE_CUSTOM, key=key, base_url="https://open.bigmodel.cn/api/coding/paas/v4/chat/completions", models="glm-4.7-flash,glm-4.5-air,glm-4.5-flash,glm-4-flash,glm-5.2", test_model=model, model_mapping="", groups=["default"], group="default", priority=0, weight=0, auto_ban=1) return dict(name=name, type=n.TYPE_CUSTOM, key=key, base_url="https://open.bigmodel.cn/api/paas/v4/chat/completions", models="glm-4-flash,glm-4.5-flash", test_model=model or "glm-4-flash", model_mapping="", groups=["default"], group="default", priority=0, weight=0, auto_ban=1) def volcano_coding_channel(name, key): return dict(name=name, type=n.TYPE_ANTHROPIC, key=key, base_url="https://ark.cn-beijing.volces.com/api/coding", models="ark-code-latest", test_model="ark-code-latest", model_mapping="", groups=["default"], group="default", priority=0, weight=0, auto_ban=1) def parse_detail(detail): m = re.match(r"(\w+)\s+([\w./\-]+)\s+200", detail) if m: return m.group(1), m.group(2) return None, None def revive_one(rec): prov = rec["prov"] kind, model = parse_detail(rec["detail"]) results = [] for cid, name, st in rec["existing"]: if st == 1: continue old = chans.get(cid, {}) # find real key key = match_key(name, prov) if not key: # for zhipu the old suffix is 4-char; try harder results.append((cid, name, "NO_KEY", "")) continue # build new channel config if prov == "zhipu": newch = zhipu_channel(name, key, kind, model) elif prov == "volcanoark" and "CodingPlan" in name: newch = volcano_coding_channel(name, key) else: # keep existing config but ensure test_model newch = dict(name=name, type=old.get("type", 1), key=key, base_url=old.get("base_url", ""), models=old.get("models", ""), test_model=model or "", model_mapping=old.get("model_mapping", ""), groups=["default"], group="default", priority=0, weight=0, auto_ban=1) # delete + recreate ok, msg = n.delete_channel(cid) if not ok: results.append((cid, name, "DEL_FAIL", msg)) continue time.sleep(0.15) ok, msg = n.add_channel(newch) if not ok: results.append((cid, name, "ADD_FAIL", msg)) continue results.append((cid, name, "RECREATED", msg)) time.sleep(0.15) return results def main(): targets = report["usable_channel_disabled"] print(f"reviving {len(targets)} disabled-live records via DELETE+RECREATE...") done = 0; ok = 0; fail = 0 log = open(H / "results/revive.log", "w") with ThreadPoolExecutor(max_workers=6) as ex: futs = [ex.submit(revive_one, rec) for rec in targets] for fut in as_completed(futs): for cid, name, status, msg in fut.result(): line = f" {cid:4} {name:28} {status} {msg}" print(line); log.write(line + "\n"); log.flush() done += 1 if status == "RECREATED": ok += 1 else: fail += 1 if done % 20 == 0: print(f" --- {done} processed ---") log.close() print(f"\n=== DONE: recreated={ok} failed={fail} ===") if __name__ == "__main__": main()