141 lines
5.1 KiB
Python
141 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
aiand_check_keys_curl.py
|
|
Validate aiand/OpenAI-compatible API keys using system curl instead of Python urllib/httpx.
|
|
|
|
Why curl: aiand's Cloudflare layer may return Error 1010 (browser signature blocked)
|
|
for Python HTTP clients even when the key is valid. curl can succeed where urllib fails.
|
|
|
|
Input formats supported:
|
|
email|sk-...|org_id
|
|
label,email,sk-...,org_id
|
|
sk-...
|
|
|
|
Output: CSV report with status classification; raw keys are never printed.
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
DEFAULT_BASE_URL = "https://api.aiand.com/v1"
|
|
DEFAULT_MODEL = "qwen/qwen3.6-27b"
|
|
|
|
|
|
def load_rows(path: Path):
|
|
rows = []
|
|
for line_no, raw in enumerate(path.read_text(encoding="utf-8", errors="ignore").splitlines(), 1):
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
sep = "|" if "|" in line else "," if "," in line else None
|
|
parts = [p.strip() for p in (line.split(sep) if sep else [line])]
|
|
email = next((p for p in parts if "@" in p), f"line_{line_no}")
|
|
key = next((p for p in parts if p.startswith("sk-")), "")
|
|
org_id = next((p for p in parts if "-" in p and len(p) >= 25 and not p.startswith("sk-") and "@" not in p), "")
|
|
rows.append({"line": line_no, "email": email, "key": key, "org_id": org_id})
|
|
return rows
|
|
|
|
|
|
def curl_chat(base_url: str, key: str, org_id: str, model: str, timeout: int):
|
|
payload = json.dumps({
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": "ping"}],
|
|
"max_tokens": 1,
|
|
"temperature": 0,
|
|
})
|
|
cmd = [
|
|
"curl", "-sS", "--max-time", str(timeout),
|
|
"-w", "\nHTTP_STATUS:%{http_code}",
|
|
base_url.rstrip("/") + "/chat/completions",
|
|
"-H", "Content-Type: application/json",
|
|
"-H", "Accept: application/json",
|
|
"-H", f"Authorization: Bearer {key}",
|
|
]
|
|
if org_id:
|
|
cmd += ["-H", f"X-Org-ID: {org_id}"]
|
|
cmd += ["--data-binary", payload]
|
|
try:
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 10)
|
|
combined = (r.stdout or "") + (("\nSTDERR:" + r.stderr) if r.stderr else "")
|
|
except Exception as exc:
|
|
return 0, repr(exc)
|
|
marker = "\nHTTP_STATUS:"
|
|
if marker not in combined:
|
|
return 0, combined.strip()
|
|
body, code = combined.rsplit(marker, 1)
|
|
try:
|
|
return int(code.strip().splitlines()[0]), body.strip()
|
|
except Exception:
|
|
return 0, body.strip()
|
|
|
|
|
|
def classify(status: int, body: str):
|
|
b = body.lower()
|
|
if status == 200:
|
|
return "ok"
|
|
if "insufficient" in b and "credit" in b:
|
|
return "valid_no_credits"
|
|
if "error 1010" in b or ("browser" in b and "signature" in b):
|
|
return "cloudflare_block"
|
|
if status in (401, 403) or "no access token" in b or "unauthorized" in b or "invalid token" in b:
|
|
return "auth_failed"
|
|
if status == 429:
|
|
return "rate_limited"
|
|
if "model" in b and any(x in b for x in ("not found", "invalid", "not exist")):
|
|
return "valid_model_problem"
|
|
if status == 0:
|
|
return "network_error"
|
|
return "unknown_error"
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--keys", required=True, help="Path to key file")
|
|
ap.add_argument("--out", default="aiand_key_report.csv", help="CSV report path")
|
|
ap.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
|
ap.add_argument("--model", default=DEFAULT_MODEL)
|
|
ap.add_argument("--timeout", type=int, default=60)
|
|
args = ap.parse_args()
|
|
|
|
key_path = Path(args.keys).expanduser()
|
|
if not key_path.exists():
|
|
print(f"ERROR: not found: {key_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
report = []
|
|
counts = {}
|
|
rows = load_rows(key_path)
|
|
for i, row in enumerate(rows, 1):
|
|
key = row["key"]
|
|
if not key or "..." in key or len(key) < 50:
|
|
status, http_status, snippet = "local_truncated_key", "", "key missing/truncated locally; not tested"
|
|
else:
|
|
http_status, body = curl_chat(args.base_url, key, row["org_id"], args.model, args.timeout)
|
|
status = classify(http_status, body)
|
|
snippet = body.replace("\n", " ")[:300]
|
|
counts[status] = counts.get(status, 0) + 1
|
|
report.append({
|
|
"email": row["email"], "line": row["line"], "key_len": len(key),
|
|
"has_org_id": bool(row["org_id"]), "status": status,
|
|
"http_status": http_status, "response_snippet": snippet,
|
|
})
|
|
print(f"[{i:03d}/{len(rows):03d}] {row['email']} len={len(key)} org={bool(row['org_id'])} status={status} http={http_status}")
|
|
|
|
out = Path(args.out)
|
|
with out.open("w", encoding="utf-8", newline="") as f:
|
|
writer = csv.DictWriter(f, fieldnames=["email", "line", "key_len", "has_org_id", "status", "http_status", "response_snippet"])
|
|
writer.writeheader()
|
|
writer.writerows(report)
|
|
|
|
print("\nSummary:")
|
|
for key, value in sorted(counts.items()):
|
|
print(f" {key}: {value}")
|
|
print(f"\nReport written: {out.resolve()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|