275 lines
9.7 KiB
Python
275 lines
9.7 KiB
Python
import sys
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
|
|
import requests
|
|
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from keycheck_common import (
|
|
append_jsonl,
|
|
commit_status_transaction,
|
|
default_input_file,
|
|
default_proxy_file,
|
|
ensure_output_files,
|
|
iter_findings,
|
|
load_checked_statuses,
|
|
load_known_keys,
|
|
load_proxies,
|
|
mask_secret,
|
|
read_plain_keys,
|
|
recover_status_transaction,
|
|
request_error_message,
|
|
record_validation_result,
|
|
require_provider_authority,
|
|
service_output_dir,
|
|
should_skip_key,
|
|
write_keycheck_event,
|
|
)
|
|
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PARENT_DIR = os.path.dirname(SCRIPT_DIR)
|
|
SERVICE = "anthropic"
|
|
OUTPUT_DIR = os.getenv("KEYCHECK_OUTPUT_DIR") or service_output_dir(SERVICE)
|
|
|
|
INPUT_FILE = os.getenv("KEYCHECK_INPUT_FILE") or default_input_file()
|
|
PROXY_FILE = os.getenv("KEYCHECK_PROXY_FILE") or default_proxy_file()
|
|
CHECKED_FILE = os.path.join(OUTPUT_DIR, "anthropicChecked.txt")
|
|
RESULTS_FILE = os.path.join(OUTPUT_DIR, "anthropicResults.jsonl")
|
|
|
|
STATUS_FILES = {
|
|
"VALID": os.path.join(OUTPUT_DIR, "anthropicAlive.txt"),
|
|
"NO_QUOTA": os.path.join(OUTPUT_DIR, "anthropicNoQuota.txt"),
|
|
"DEAD": os.path.join(OUTPUT_DIR, "anthropicDead.txt"),
|
|
"LIMITED": os.path.join(OUTPUT_DIR, "anthropicLimited.txt"),
|
|
"RESTRICTED": os.path.join(OUTPUT_DIR, "anthropicRestricted.txt"),
|
|
"NETWORK": os.path.join(OUTPUT_DIR, "anthropicNetwork.txt"),
|
|
"UNKNOWN": os.path.join(OUTPUT_DIR, "anthropicUnknown.txt"),
|
|
}
|
|
|
|
ANTHROPIC_REGEX = re.compile(r"sk-ant-(?:api03|admin01)-[A-Za-z0-9\-_]{93}AA|sk-ant-[A-Za-z0-9\-_]{86}")
|
|
ANTHROPIC_ADMIN_PREFIX = "sk-ant-admin01-"
|
|
ANTHROPIC_ADMIN_API_KEYS_URL = "https://api.anthropic.com/v1/organizations/api_keys"
|
|
|
|
|
|
def ensure_files():
|
|
ensure_output_files([CHECKED_FILE, RESULTS_FILE, *STATUS_FILES.values()])
|
|
recover_status_transaction(CHECKED_FILE, STATUS_FILES)
|
|
|
|
|
|
def extract_candidates(input_file, plain_files):
|
|
seen_plain = set()
|
|
for item in iter_findings(input_file, ["Anthropic"]):
|
|
key = item["raw"]
|
|
if key and ANTHROPIC_REGEX.fullmatch(key):
|
|
yield key, item["source"], item["finding"]
|
|
for item in read_plain_keys(plain_files, ANTHROPIC_REGEX):
|
|
key = item["key"]
|
|
if key not in seen_plain:
|
|
seen_plain.add(key)
|
|
yield key, item["source"], {}
|
|
|
|
|
|
def tier_from_rpm(rpm):
|
|
mapping = {5: "Free Tier", 50: "Tier 1", 1000: "Tier 2", 2000: "Tier 3", 4000: "Tier 4"}
|
|
return mapping.get(rpm, "Scale/Unknown")
|
|
|
|
|
|
def anthropic_rate_headers(response):
|
|
output = {}
|
|
for name, value in response.headers.items():
|
|
lowered = name.lower()
|
|
if lowered.startswith("anthropic-ratelimit-"):
|
|
output[lowered.replace("anthropic-ratelimit-", "rate_").replace("-", "_")] = value
|
|
return output
|
|
|
|
|
|
def list_models(key, proxy, timeout):
|
|
headers = {
|
|
"anthropic-version": "2023-06-01",
|
|
"x-api-key": key,
|
|
}
|
|
try:
|
|
response = requests.get("https://api.anthropic.com/v1/models", headers=headers, proxies=proxy, timeout=timeout)
|
|
except requests.RequestException as exc:
|
|
return {"models_error": str(exc)[:500]}
|
|
if response.status_code != 200:
|
|
return {"models_status": response.status_code, "models_error": request_error_message(response)}
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
return {"models_status": response.status_code, "models_error": "invalid JSON response"}
|
|
models = []
|
|
for item in data.get("data") or []:
|
|
if isinstance(item, dict) and item.get("id"):
|
|
models.append(item["id"])
|
|
return {
|
|
"models_status": response.status_code,
|
|
"models_count": len(models),
|
|
"models": models[:50],
|
|
}
|
|
|
|
|
|
def check_admin_key(key, proxy, timeout):
|
|
headers = {
|
|
"anthropic-version": "2023-06-01",
|
|
"x-api-key": key,
|
|
}
|
|
try:
|
|
response = requests.get(
|
|
ANTHROPIC_ADMIN_API_KEYS_URL,
|
|
headers=headers,
|
|
params={"limit": 1},
|
|
proxies=proxy,
|
|
timeout=timeout,
|
|
)
|
|
except requests.RequestException as exc:
|
|
return {"status": "NETWORK", "admin": True, "message": str(exc)[:500]}
|
|
|
|
if response.status_code == 200:
|
|
return {
|
|
"status": "VALID",
|
|
"admin": True,
|
|
"http_status": 200,
|
|
"message": "Anthropic Admin API access confirmed",
|
|
}
|
|
|
|
status = {
|
|
401: "DEAD",
|
|
403: "RESTRICTED",
|
|
429: "LIMITED",
|
|
}.get(response.status_code, "UNKNOWN")
|
|
message = request_error_message(response).replace(key, "***REDACTED***")
|
|
return {
|
|
"status": status,
|
|
"admin": True,
|
|
"http_status": response.status_code,
|
|
"message": message,
|
|
}
|
|
|
|
|
|
def check_key(key, proxy, timeout, model="claude-opus-4-6", include_models=False):
|
|
if key.startswith(ANTHROPIC_ADMIN_PREFIX):
|
|
return check_admin_key(key, proxy, timeout)
|
|
|
|
url = "https://api.anthropic.com/v1/messages"
|
|
headers = {
|
|
"content-type": "application/json",
|
|
"anthropic-version": "2023-06-01",
|
|
"x-api-key": key,
|
|
}
|
|
payload = {
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": "ping"}],
|
|
"max_tokens": 1,
|
|
}
|
|
try:
|
|
response = requests.post(url, headers=headers, json=payload, proxies=proxy, timeout=timeout)
|
|
except requests.RequestException as exc:
|
|
return {"status": "NETWORK", "message": str(exc)}
|
|
|
|
if response.status_code == 200:
|
|
rpm = 0
|
|
try:
|
|
rpm = int(response.headers.get("anthropic-ratelimit-requests-limit", "0"))
|
|
except ValueError:
|
|
rpm = 0
|
|
rate_data = anthropic_rate_headers(response)
|
|
result = {
|
|
"status": "VALID",
|
|
"model": model,
|
|
"rpm": rpm,
|
|
"tier": tier_from_rpm(rpm),
|
|
**rate_data,
|
|
}
|
|
if include_models:
|
|
result.update(list_models(key, proxy, timeout))
|
|
token_limit = rate_data.get("rate_tokens_limit") or ""
|
|
token_remaining = rate_data.get("rate_tokens_remaining") or ""
|
|
token_part = f" tokens={token_remaining}/{token_limit}" if token_limit or token_remaining else ""
|
|
result["message"] = f"model={model}; rpm={rpm}; tier={result['tier']}{token_part}"
|
|
return result
|
|
|
|
if response.status_code == 429:
|
|
return {"status": "LIMITED", "http_status": 429, "message": request_error_message(response)}
|
|
|
|
message = request_error_message(response)
|
|
lower = message.lower()
|
|
if "credit balance is too low" in lower or "usage limits" in lower:
|
|
return {"status": "NO_QUOTA", "http_status": response.status_code, "message": message}
|
|
if response.status_code in (401, 403):
|
|
status = "RESTRICTED" if "disabled" in lower or response.status_code == 403 else "DEAD"
|
|
return {"status": status, "http_status": response.status_code, "message": message}
|
|
return {"status": "UNKNOWN", "http_status": response.status_code, "message": message}
|
|
|
|
|
|
def write_result(key, result, source, finding):
|
|
write_keycheck_event(SERVICE, RESULTS_FILE, key, result, source, finding, "Anthropic")
|
|
commit_status_transaction(
|
|
CHECKED_FILE, STATUS_FILES, key, result["status"], result.get("message", ""), source,
|
|
)
|
|
record_validation_result(SERVICE, key, result, source, finding, "Anthropic")
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Anthropic key checker")
|
|
parser.add_argument("--input", default=INPUT_FILE)
|
|
parser.add_argument("--plain", action="append", default=[])
|
|
parser.add_argument("--proxy-file", default=PROXY_FILE)
|
|
parser.add_argument("--timeout", type=int, default=20)
|
|
parser.add_argument("--model", default=os.getenv("ANTHROPIC_CHECK_MODEL", "claude-opus-4-6"))
|
|
parser.add_argument("--list-models", action="store_true")
|
|
parser.add_argument("--max-keys", type=int, default=0)
|
|
parser.add_argument("--retry-network", action="store_true")
|
|
parser.add_argument("--retry-limited", action="store_true")
|
|
parser.add_argument("--retry-unknown", action="store_true")
|
|
parser.add_argument("--retry-no-balance", action="store_true")
|
|
parser.add_argument("--recheck-all", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
require_provider_authority(SERVICE)
|
|
args = parse_args()
|
|
ensure_files()
|
|
proxy_cycler = load_proxies(args.proxy_file)
|
|
checked = load_checked_statuses(CHECKED_FILE)
|
|
known = load_known_keys(CHECKED_FILE, STATUS_FILES)
|
|
retry_statuses = set()
|
|
if args.retry_network:
|
|
retry_statuses.add("NETWORK")
|
|
if args.retry_limited:
|
|
retry_statuses.add("LIMITED")
|
|
if args.retry_unknown:
|
|
retry_statuses.add("UNKNOWN")
|
|
if args.retry_no_balance:
|
|
retry_statuses.add("NO_QUOTA")
|
|
|
|
processed = 0
|
|
skipped = 0
|
|
for key, source, finding in extract_candidates(args.input, args.plain):
|
|
if should_skip_key(key, checked, known, args, retry_statuses, service=SERVICE, source=source, finding=finding, detector="Anthropic"):
|
|
skipped += 1
|
|
continue
|
|
if args.max_keys and processed >= args.max_keys:
|
|
break
|
|
processed += 1
|
|
print(f"\n[{processed}] Anthropic candidate {mask_secret(key)} from {source}")
|
|
proxy = next(proxy_cycler) if proxy_cycler else None
|
|
result = check_key(key, proxy, args.timeout, args.model, args.list_models)
|
|
print(f" STATUS: {result['status']} | {result.get('message', '')[:200]}")
|
|
write_result(key, result, source, finding)
|
|
known.add(key)
|
|
checked[key] = result["status"]
|
|
|
|
print(f"\nDone. Processed={processed}, skipped={skipped}, results={RESULTS_FILE}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|