171 lines
5.5 KiB
Python
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()
|