143 lines
5.4 KiB
Python
143 lines
5.4 KiB
Python
"""apply_ds_audit.py — write multi-region probe verdicts back into keys.db.
|
|
|
|
audit_ds_multiregion.py proved the stored statuses are wrong in both directions:
|
|
8 rows marked DEAD authenticate fine (region-locked keys probed only on Beijing
|
|
before), and 31 rows marked WORKING now reject everywhere. This applies the
|
|
verdicts, using the closed status vocabulary the rest of the farm consumes
|
|
(NEW/WORKING/DEAD/LIMITED/FREE/ERR) and putting the fine class in `plan`.
|
|
|
|
Never marks DEAD from a transport/endpoint verdict: net and endpoint_error leave
|
|
the row untouched for the next pass.
|
|
|
|
Run: python apply_ds_audit.py [--audit PATH] [--dry]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
DB = os.path.join(HERE, "data", "keys.db")
|
|
|
|
# probe verdict -> (status, plan prefix); None status means "do not touch"
|
|
VERDICT = {
|
|
"WORKING": ("WORKING", "auth-ok"),
|
|
"arrearage": ("LIMITED", "arrearage"),
|
|
"free_tier": ("LIMITED", "free-tier-exhausted"),
|
|
"reject": ("DEAD", "rejected-all-regions"),
|
|
"throttled": (None, "throttled"), # caller/IP side: retry later
|
|
"net": (None, "net"),
|
|
"endpoint_error": (None, "endpoint"),
|
|
"error": (None, "probe-error"),
|
|
}
|
|
|
|
|
|
def latest_audit(explicit: str | None) -> str:
|
|
if explicit:
|
|
return explicit
|
|
cands = sorted(glob.glob(os.path.join(os.path.dirname(HERE),
|
|
"ds_multiregion_audit_*.json")))
|
|
cands += sorted(glob.glob(os.path.join(HERE, "ds_multiregion_audit_*.json")))
|
|
if not cands:
|
|
sys.exit("no ds_multiregion_audit_*.json found — run audit_ds_multiregion.py first")
|
|
return cands[-1]
|
|
|
|
|
|
# a positive probe proves the real provider; the regex guess in `prov` cannot
|
|
# separate DASHSCOPE from DEEPSEEK (identical sk-<32hex> shape)
|
|
PROOF = (("dashscope", "DASHSCOPE"), ("deepseek", "DEEPSEEK"), ("zai", "ZAI"),
|
|
("zhipu", "ZAI"), ("bigmodel", "ZAI"), ("moonshot", "MOONSHOT"),
|
|
("kimi", "KIMI"), ("siliconflow", "SILICONFLOW"))
|
|
|
|
|
|
def proved_provider(info):
|
|
head = str(info or "").strip().lower()
|
|
for needle, prov in PROOF:
|
|
if head.startswith(needle):
|
|
return prov
|
|
return None
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--audit", default=None)
|
|
ap.add_argument("--dry", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
path = latest_audit(args.audit)
|
|
data = json.load(open(path, encoding="utf-8"))
|
|
rows = data.get("results") or []
|
|
print(f"audit: {path} ({len(rows)} verdicts, generated {data.get('generated')})")
|
|
|
|
conn = sqlite3.connect(DB, timeout=60)
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA busy_timeout=30000")
|
|
before = dict(conn.execute("SELECT status, COUNT(*) FROM keys GROUP BY status"))
|
|
changes: dict[str, int] = {}
|
|
skipped = 0
|
|
now = time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
for r in rows:
|
|
h = r.get("hash")
|
|
verdict = r.get("probe_status")
|
|
mapping = VERDICT.get(verdict)
|
|
if not h or mapping is None:
|
|
skipped += 1
|
|
continue
|
|
status, prefix = mapping
|
|
if status == "WORKING" and r.get("usable") is False:
|
|
# authenticated but cannot run inference (arrears / free tier spent):
|
|
# LIMITED keeps it out of rotators without calling the key dead
|
|
status = "LIMITED"
|
|
prefix = str(r.get("usable_info") or "unusable")[:40]
|
|
if status is None:
|
|
skipped += 1 # transient: leave for the next pass
|
|
continue
|
|
cur = conn.execute("SELECT status FROM keys WHERE hash=?", (h,)).fetchone()
|
|
if not cur:
|
|
skipped += 1
|
|
continue
|
|
if cur[0] == status:
|
|
continue
|
|
plan = f"{prefix}: {str(r.get('info') or '')[:150]}"
|
|
proved = proved_provider(r.get("info")) if status == "WORKING" else None
|
|
if args.dry:
|
|
changes[f"{cur[0]}->{status}"] = changes.get(f"{cur[0]}->{status}", 0) + 1
|
|
continue
|
|
if proved:
|
|
conn.execute("UPDATE keys SET status=?, prov=?, plan=?, remaining=? "
|
|
"WHERE hash=?", (status, proved, plan,
|
|
"OK" if status == "WORKING" else None, h))
|
|
else:
|
|
conn.execute("UPDATE keys SET status=?, plan=?, remaining=? WHERE hash=?",
|
|
(status, plan, "OK" if status == "WORKING" else None, h))
|
|
changes[f"{cur[0]}->{status}"] = changes.get(f"{cur[0]}->{status}", 0) + 1
|
|
|
|
if not args.dry and sum(changes.values()):
|
|
# snapshot BEFORE committing — this copy is the rollback point
|
|
bak = DB + ".bak-apply-" + time.strftime("%Y%m%d_%H%M%S")
|
|
shutil.copy2(DB, bak)
|
|
print("backup:", bak)
|
|
conn.commit()
|
|
elif not args.dry:
|
|
conn.commit()
|
|
|
|
after = dict(conn.execute("SELECT status, COUNT(*) FROM keys GROUP BY status"))
|
|
conn.close()
|
|
print("skipped (transient/absent):", skipped)
|
|
print("transitions:", json.dumps(changes, ensure_ascii=False) or "{}")
|
|
print("status counts before:", json.dumps(before, ensure_ascii=False))
|
|
print("status counts after :", json.dumps(after, ensure_ascii=False))
|
|
print("DRY RUN — nothing written" if args.dry else "APPLIED")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|