#!/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: providerkeyurldetail 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()