#!/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()