Files
hack/tools/scripts/audit_channels.py
T

171 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Audit: collect every USABLE key from (a) vault keys.json and (b) all
results/*/usable.txt buckets, dedup against keys already in NewAPI, and emit
a per-provider list of candidates that need a channel.
Does NOT create anything. Writes results/channel_audit.json.
"""
import json, re, sys
from pathlib import Path
from collections import defaultdict
sys.path.insert(0, str(Path(__file__).resolve().parent))
import newapi_client as n
H = Path(__file__).resolve().parent / "llm-key-hunter"
VAULT = H / "usable_keys"
RESULTS = H / "results"
# provider label normalisation -> vault dir
def norm(label):
l = label.lower().replace(" ", "").replace("_", "").replace("-", "")
m = {
"minimax": "minimax", "deepseek": "deepseek", "kimi": "kimi",
"moonshot": "kimi", "siliconflow": "siliconflow", "zhipuai": "zhipu",
"zhipu": "zhipu", "bigmodel": "zhipu", "volcanoark": "volcanoark",
"volcano": "volcanoark", "volcengine": "volcanoark", "doubao": "volcanoark",
"longcat": "longcat", "dashscope": "dashscope", "qwen": "dashscope",
"freemodel": "freemodel", "xunfei": "xunfei", "iflytek": "xunfei",
"spark": "xunfei", "codingplan": "codingplan", "opencode": "codingplan",
"mimo": "mimo", "xiaomimimo": "mimo", "kiro": "kiro",
}
return m.get(l)
def load_vault_keys():
out = defaultdict(list)
for d in sorted(VAULT.iterdir()):
kj = d / "keys.json"
if not d.is_dir() or not kj.exists():
continue
try:
data = json.loads(kj.read_text())
except Exception:
continue
for e in data:
k = e.get("key")
if k:
out[d.name].append((k, e.get("source", "")))
return out
def parse_usable_line(line):
line = line.strip()
if not line or line.startswith("#"):
return None
# tab-separated: provider<TAB>key<TAB>url<TAB>detail
if "\t" in line:
parts = line.split("\t")
if len(parts) >= 2:
return parts[0], parts[1], parts[2] if len(parts) > 2 else ""
# pipe-separated: Provider|key|url|desc
if "|" in line:
parts = line.split("|")
if len(parts) >= 2:
return parts[0], parts[1], parts[2] if len(parts) > 2 else ""
return None
def load_bucket_keys():
"""keys from all results/*/usable.txt that may not be vaulted yet."""
out = defaultdict(dict) # vault_dir -> {key: source}
for f in sorted(RESULTS.glob("*/usable.txt")):
for line in f.read_text().splitlines():
r = parse_usable_line(line)
if not r:
continue
label, key, src = r
vd = norm(label)
if vd:
out[vd][key] = src
return out
def newapi_keys():
keys = set()
for ch in n.list_channels():
try:
d = n.get_channel(ch["id"])
if d and d.get("key"):
keys.add(d["key"])
except Exception:
pass
return keys
def channel_status_summary():
"""count channels by status across all IDs."""
counts = defaultdict(int)
disabled = []
for ch in n.list_channels():
cid = ch["id"]
try:
d = n.get_channel(cid)
except Exception:
continue
st = d.get("status", 0)
counts[st] += 1
if st != 1: # 1=enabled
disabled.append((cid, d.get("name", ""), st))
return counts, disabled
def main():
print("Loading NewAPI channel keys (this scans all channels)...")
existing = newapi_keys()
print(f" {len(existing)} unique keys currently in NewAPI")
counts, disabled = channel_status_summary()
print(f" channel status: {dict(counts)} (1=enabled, 2=manual-disabled, 3=auto-disabled)")
vault = load_vault_keys()
buckets = load_bucket_keys()
missing = defaultdict(list) # vault_dir -> [(key,source)]
for vd, items in vault.items():
for key, src in items:
if key not in existing:
missing[vd].append((key, src))
# bucket keys not in vault and not in NewAPI
extra = defaultdict(list)
vault_keyset = {k for items in vault.values() for k, _ in items}
for vd, kmap in buckets.items():
for key, src in kmap.items():
if key not in vault_keyset and key not in existing:
extra[vd].append((key, src))
report = {
"existing_newapi_keys": len(existing),
"channel_status": dict(counts),
"disabled_channels": disabled,
"vault_not_in_newapi": {k: v for k, v in missing.items()},
"bucket_not_in_vault_or_newapi": {k: v for k, v in extra.items()},
}
out = RESULTS / "channel_audit.json"
out.write_text(json.dumps(report, indent=2, ensure_ascii=False))
print("\n=== vault keys WITHOUT a NewAPI channel ===")
tot = 0
for vd, items in sorted(missing.items()):
print(f" {vd:14} {len(items)}")
tot += len(items)
print(f" TOTAL missing: {tot}")
print("\n=== bucket usable keys NOT vaulted & NOT in NewAPI ===")
te = 0
for vd, items in sorted(extra.items()):
print(f" {vd:14} {len(items)}")
te += len(items)
print(f" TOTAL extra: {te}")
print(f"\n=== disabled/abnormal channels: {len(disabled)} ===")
for cid, name, st in disabled[:30]:
print(f" {cid} {name:30} status={st}")
if len(disabled) > 30:
print(f" ... and {len(disabled)-30} more")
print(f"\naudit written -> {out}")
if __name__ == "__main__":
main()