#!/usr/bin/env python """keyfarm_rotator.py — multi-provider live-key gateway on 127.0.0.1:16433. Pools WORKING/FREE/LIMITED keys from keyhunter data/keys.db every 120s and round-robins them across OpenAI-compatible upstreams. 401/402/429 -> cooldown, next key. OMP registers this as provider `keyfarm` (baseUrl /v1). Providers: deepseek, openrouter, mistral, google (gemini openai-compat). Run: python keyfarm_rotator.py """ import http.server import json import os import sqlite3 import threading import time import urllib.request import urllib.error DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "keys.db") HOST, PORT = "127.0.0.1", 16436 # models.yml: hermes-deepseek (deepseek-pool-rotated) REFRESH = 120 # seconds between pool reloads COOLDOWN = 1800 # seconds a failing key sits out MAX_TRIES = 4 # keys tried per request # Only OpenAI-compatible providers. Non-compat (anthropic, cohere) excluded. PROVIDERS = { "deepseek": ("https://api.deepseek.com/v1", "sk-", ("deepseek-chat", "deepseek-reasoner")), "openrouter": ("https://openrouter.ai/api/v1", "sk-or-", ("openrouter/",)), "mistral": ("https://api.mistral.ai/v1", "", ("mistral-",)), "google": ("https://generativelanguage.googleapis.com/v1beta/openai", "AI", ("gemini-",)), "dashscope": ("https://dashscope.aliyuncs.com/compatible-mode/v1", "sk-", ("qwen-",)), "zai": ("https://api.z.ai/api/coding/paas/v4", "", ("glm-",)), "kimi": ("https://api.moonshot.cn/v1", "sk-", ("kimi-",)), "siliconflow":("https://api.siliconflow.cn/v1", "sk-", ("siliconflow-",)), "together": ("https://api.together.xyz/v1", "", ("together-",)), "deepinfra": ("https://api.deepinfra.com/v1/openai", "", ("deepinfra-",)), "nvidia": ("https://integrate.api.nvidia.com/v1", "nvapi-", ("nvidia-",)), "groq": ("https://api.groq.com/openai/v1", "gsk_", ("groq-",)), "perplexity": ("https://api.perplexity.ai", "pplx-", ("perplexity-",)), "fireworks": ("https://api.fireworks.ai/inference/v1", "fw_", ("fireworks-",)), "cerebras": ("https://api.cerebras.ai/v1", "", ("cerebras-",)), "novita": ("https://api.novita.ai/v3/openai", "", ("novita-",)), "openai": ("https://api.openai.com/v1", "sk-", ("gpt-", "o1", "o3", "o4")), "xai": ("https://api.x.ai/v1", "xai-", ("grok-",)), } DB_PROV = { "deepseek": "DEEPSEEK", "openrouter": "OPENROUTER", "mistral": "MISTRAL", "google": "GOOGLE", "dashscope": "DASHSCOPE", "zai": "ZAI", "kimi": "KIMI", "siliconflow": "SILICONFLOW", "together": "TOGETHER", "deepinfra": "DEEPINFRA", "nvidia": "NVIDIA", "groq": "GROQ", "perplexity": "PERPLEXITY", "fireworks": "FIREWORKS", "cerebras": "CEREBRAS", "novita": "NOVITA", "openai": "OPENAI", "xai": "GROK", } EXTRA_HEADERS = { # openrouter wants these "openrouter": {"HTTP-Referer": "https://keyfarm.local", "X-Title": "keyfarm"}, } lock = threading.Lock() state = {p: {"keys": [], "idx": 0, "cooldowns": {}, "ok": 0, "fail": 0} for p in PROVIDERS} stats = {"last_refresh": None, "last_error": None} def log(msg): print(time.strftime("[%H:%M:%S]") + " " + msg, flush=True) def refresh_pools(): if not os.path.exists(DB): stats["last_error"] = "no keys.db" return db = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=60) try: for p, prov_name in DB_PROV.items(): prefix = PROVIDERS[p][1] rows = db.execute( "SELECT val FROM keys WHERE prov=? AND status IN " "('WORKING','FREE','LIMITED') AND val LIKE ?", (prov_name, prefix + "%")).fetchall() with lock: old = set(state[p]["keys"]) dead = state[p].get("dead", set()) now = time.time() cd = {k: t for k, t in state[p]["cooldowns"].items() if now - t < COOLDOWN} state[p]["cooldowns"] = cd fresh = [r[0] for r in rows if r[0] not in cd and r[0] not in dead] added = len(set(fresh) - old) state[p]["keys"] = fresh if added: log(f"pool {p}: +{added} -> {len(fresh)} live") stats["last_refresh"] = time.strftime("%H:%M:%S") except Exception as e: stats["last_error"] = repr(e) log(f"refresh ERR {e!r}") finally: db.close() def pick_key(pool, tried): with lock: keys = [k for k in pool["keys"] if k not in tried] if not keys: return None pool["idx"] = (pool["idx"] + 1) % len(keys) return keys[pool["idx"] % len(keys)] _DEAD_STREAKS = {} _DEAD_STREAK_LIMIT = 3 def bench(pool, key, dead=False, prov=None): """Remove key from pool. dead=True marks DEAD in keys.db only after _DEAD_STREAK_LIMIT consecutive dead marks (one 401-like must not permanently kill a key for all consumers).""" with lock: if dead: pool["dead"] = pool.get("dead", set()) | {key} else: pool["cooldowns"][key] = time.time() _DEAD_STREAKS.pop(key, None) if key in pool["keys"]: pool["keys"].remove(key) if dead: streak_key = (prov, key) if prov else key n = _DEAD_STREAKS[streak_key] = _DEAD_STREAKS.get(streak_key, 0) + 1 if n < _DEAD_STREAK_LIMIT: log(f"key dead-streak {n}/{_DEAD_STREAK_LIMIT} (not marked in DB yet)") return if prov and prov in DB_PROV: try: dbw = sqlite3.connect(DB, timeout=2) dbw.execute("UPDATE keys SET status='DEAD' WHERE prov=? AND val=?", (DB_PROV[prov], key)) dbw.commit() dbw.close() except Exception as e: log(f"DB mark-dead ERR {e!r}") else: log(f"skip DB mark-dead: unknown prov={prov!r} (key benched in-memory only)") log(f"key {'DEAD' if dead else 'benched'} ({len(pool['keys'])} left)") def route(model): for p, (_, _, prefixes) in PROVIDERS.items(): if any(model.startswith(x) for x in prefixes): return p # bare model names fall back by heuristics if any(m in model.lower() for m in ("kimi", "glm", "qwen")): return "openrouter" # often routed via OR return None def forward(provider, key, body_bytes, api_key_hdr): base = PROVIDERS[provider][0] req = urllib.request.Request(base + "/chat/completions", data=body_bytes, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {key}", "Accept": body_accept(body_bytes), **EXTRA_HEADERS.get(provider, {}), }, method="POST") try: return urllib.request.urlopen(req, timeout=300) except urllib.error.HTTPError as e: return e def body_accept(body_bytes): try: if json.loads(body_bytes).get("stream"): return "text/event-stream" except Exception: pass return "application/json" class Handler(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def log_message(self, *a): pass def _json(self, code, obj): raw = json.dumps(obj).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self): if self.path.startswith("/v1/models"): out = [{"id": m, "object": "model"} for m in models_list()] return self._json(200, {"object": "list", "data": out}) # dashboard with lock: pools = {p: {"live": len(s["keys"]), "ok": s["ok"], "fail": s["fail"]} for p, s in state.items()} self._json(200, {"pools": pools, **stats}) def do_POST(self): if not self.path.startswith("/v1/chat/completions"): return self._json(404, {"error": "not found"}) n = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(n) if n else b"{}" try: model = json.loads(body).get("model", "") except Exception: return self._json(400, {"error": "bad json"}) provider = route(model) if not provider or not state[provider]["keys"]: return self._json(503, {"error": f"no live keys for model {model!r}"}) tried = set() for _ in range(MAX_TRIES): key = pick_key(state[provider], tried) if not key: break tried.add(key) resp = forward(provider, key, body, None) code = getattr(resp, "code", resp.status if hasattr(resp, "status") else 200) if code in (401, 402): # invalid / zero balance -> permanent DEAD bench(state[provider], key, dead=True, prov=provider) elif code == 429: # rate limit -> temp cooldown bench(state[provider], key, prov=provider) else: pass # fall through to stream if code in (401, 402, 429): with lock: state[provider]["fail"] += 1 continue # success or upstream app-level error: stream through with lock: state[provider]["ok"] += 1 try: self.send_response(code) ctype = resp.headers.get("Content-Type", "application/json") self.send_header("Content-Type", ctype) if resp.headers.get("Content-Length"): self.send_header("Content-Length", resp.headers["Content-Length"]) else: self.send_header("Transfer-Encoding", "chunked") self.end_headers() if "chunked" in (resp.headers.get("Transfer-Encoding") or ""): self._pump_chunked(resp) else: while True: chunk = resp.read(65536) if not chunk: break self.wfile.write(chunk) self.wfile.flush() return except Exception as e: log(f"stream ERR {e!r}") return self._json(502, {"error": f"all {len(tried)} keys failed for {model!r}"}) def _pump_chunked(self, resp): # upstream chunked SSE -> forward raw (we already set chunked) while True: chunk = resp.read(65536) if not chunk: break self.wfile.write(b"%x\r\n" % len(chunk) + chunk + b"\r\n") self.wfile.flush() self.wfile.write(b"0\r\n\r\n") def models_list(): return [ "deepseek-chat", "deepseek-reasoner", "openrouter/deepseek/deepseek-chat", "openrouter/anthropic/claude-sonnet-4.6", "openrouter/openai/gpt-5.4-mini", "mistral-large-latest", "mistral-small-latest", "gemini-2.0-flash", "gemini-2.5-flash", ] def refresher(): while True: refresh_pools() time.sleep(REFRESH) def main(): refresh_pools() threading.Thread(target=refresher, daemon=True).start() srv = http.server.ThreadingHTTPServer((HOST, PORT), Handler) log(f"keyfarm rotator on {HOST}:{PORT}") srv.serve_forever() if __name__ == "__main__": main()