- 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)
185 lines
5.7 KiB
Python
185 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Import all usable keys into NewAPI - one channel per key.
|
|
|
|
Channel name format: <Provider>_<last4ofkey>
|
|
No merging/pooling - each key gets its own channel.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
# Add scripts dir to path for imports
|
|
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
from newapi_client import (
|
|
add_channel, list_channel_names,
|
|
TYPE_OPENAI, TYPE_ANTHROPIC,
|
|
)
|
|
|
|
# ── Provider configs ────────────────────────────────────────────
|
|
PROVIDER_CONFIGS = {
|
|
"ZhipuAI": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
"models": "glm-4v-flash,glm-4-flash,glm-4,glm-4-plus,glm-4-long,glm-4.5,glm-4.6,glm-4.7",
|
|
},
|
|
"VolcanoArk": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
|
|
"models": (
|
|
"doubao-seed-2-0-pro-260215,doubao-seed-2-0-lite-260215,"
|
|
"doubao-seed-2-0-mini-260215,doubao-seed-2-1-pro-260628,"
|
|
"doubao-seed-2-1-turbo-260628,doubao-seed-1-6-250615,"
|
|
"doubao-seed-1-6-251015,doubao-seed-1-6-flash-250615,"
|
|
"doubao-seed-1-6-flash-250828,deepseek-v4-flash-260425,"
|
|
"deepseek-v4-pro-260425,glm-5-2-260617"
|
|
),
|
|
},
|
|
"MiniMax": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://api.minimaxi.com/v1",
|
|
"models": "MiniMax-M2.5,MiniMax-M3,MiniMax-M2.7",
|
|
},
|
|
"DashScope": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
"models": "qwen-plus,qwen-turbo,qwen-max,qwen3.7max",
|
|
},
|
|
"LongCat": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://api.longcat.chat/openai",
|
|
"models": "LongCat-2.0",
|
|
},
|
|
"SiliconFlow": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://api.siliconflow.cn/v1",
|
|
"models": "Qwen/Qwen2.5-7B-Instruct,deepseek-ai/DeepSeek-V3",
|
|
},
|
|
"DeepSeek": {
|
|
"type": TYPE_OPENAI,
|
|
"base_url": "https://api.deepseek.com/v1",
|
|
"models": "deepseek-chat,deepseek-reasoner",
|
|
},
|
|
}
|
|
|
|
USABLE_FILE = Path(__file__).parent / "llm-key-hunter" / "results" / "deep_verify" / "usable.txt"
|
|
|
|
|
|
def load_usable_keys():
|
|
"""Load usable keys from file. Returns list of (provider, key, url, desc)."""
|
|
keys = []
|
|
with open(USABLE_FILE) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split("|")
|
|
if len(parts) >= 2:
|
|
provider = parts[0]
|
|
key = parts[1]
|
|
url = parts[2] if len(parts) > 2 else ""
|
|
desc = parts[3] if len(parts) > 3 else ""
|
|
keys.append((provider, key, url, desc))
|
|
return keys
|
|
|
|
|
|
def build_channel(provider, key):
|
|
"""Build a channel dict for a single key."""
|
|
cfg = PROVIDER_CONFIGS.get(provider)
|
|
if not cfg:
|
|
return None
|
|
|
|
last4 = key[-4:]
|
|
name = f"{provider}_{last4}"
|
|
|
|
return {
|
|
"type": cfg["type"],
|
|
"name": name,
|
|
"key": key,
|
|
"base_url": cfg["base_url"],
|
|
"models": cfg["models"],
|
|
"group": "default",
|
|
}
|
|
|
|
|
|
def import_single_channel(provider, key):
|
|
"""Import a single key as a channel. Returns (name, success, message)."""
|
|
channel = build_channel(provider, key)
|
|
if not channel:
|
|
return (channel["name"] if channel else "?", False, f"Unknown provider: {provider}")
|
|
|
|
name = channel["name"]
|
|
try:
|
|
success, message = add_channel(channel, mode="single")
|
|
return (name, success, message)
|
|
except Exception as e:
|
|
return (name, False, str(e))
|
|
|
|
|
|
def main():
|
|
keys = load_usable_keys()
|
|
print(f"Loaded {len(keys)} usable keys")
|
|
|
|
# Check existing channel names for dedup
|
|
try:
|
|
existing = list_channel_names()
|
|
print(f"Existing channels: {len(existing)}")
|
|
except Exception as e:
|
|
print(f"Warning: Could not list existing channels: {e}")
|
|
existing = set()
|
|
|
|
# Filter out providers we don't have configs for
|
|
to_import = []
|
|
skipped = []
|
|
for provider, key, url, desc in keys:
|
|
cfg = PROVIDER_CONFIGS.get(provider)
|
|
if not cfg:
|
|
skipped.append((provider, key))
|
|
continue
|
|
last4 = key[-4:]
|
|
name = f"{provider}_{last4}"
|
|
if name in existing:
|
|
skipped.append((provider, key))
|
|
continue
|
|
to_import.append((provider, key))
|
|
|
|
if skipped:
|
|
print(f"Skipping {len(skipped)} keys (unknown provider or duplicate name)")
|
|
print(f"To import: {len(to_import)} channels")
|
|
|
|
# Import with concurrency (but not too high - NewAPI might rate limit)
|
|
success_count = 0
|
|
fail_count = 0
|
|
start = time.time()
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {
|
|
pool.submit(import_single_channel, provider, key): (provider, key)
|
|
for provider, key in to_import
|
|
}
|
|
|
|
for i, future in enumerate(as_completed(futures), 1):
|
|
provider, key = futures[future]
|
|
name, success, message = future.result()
|
|
|
|
if success:
|
|
success_count += 1
|
|
print(f" [{i}/{len(to_import)}] ✓ {name}")
|
|
else:
|
|
fail_count += 1
|
|
print(f" [{i}/{len(to_import)}] ✗ {name}: {message}")
|
|
|
|
elapsed = time.time() - start
|
|
print(f"\n=== Import complete ({elapsed:.1f}s) ===")
|
|
print(f" Success: {success_count}")
|
|
print(f" Failed: {fail_count}")
|
|
print(f" Skipped: {len(skipped)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|