276 lines
9.8 KiB
Python
276 lines
9.8 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,
|
|
classify_common_http_status,
|
|
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,
|
|
record_validation_result,
|
|
recover_status_transaction,
|
|
request_error_message,
|
|
require_provider_authority,
|
|
service_output_dir,
|
|
should_skip_key,
|
|
write_keycheck_event,
|
|
)
|
|
|
|
|
|
SERVICE = "groq"
|
|
DETECTOR = "Groq"
|
|
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, "groqChecked.txt")
|
|
RESULTS_FILE = os.path.join(OUTPUT_DIR, "groqResults.jsonl")
|
|
STATUS_FILES = {
|
|
"VALID": os.path.join(OUTPUT_DIR, "groqAlive.txt"),
|
|
"NO_BALANCE": os.path.join(OUTPUT_DIR, "groqNoBalance.txt"),
|
|
"DEAD": os.path.join(OUTPUT_DIR, "groqDead.txt"),
|
|
"RESTRICTED": os.path.join(OUTPUT_DIR, "groqRestricted.txt"),
|
|
"LIMITED": os.path.join(OUTPUT_DIR, "groqLimited.txt"),
|
|
"NO_CONTEXT": os.path.join(OUTPUT_DIR, "groqNoContext.txt"),
|
|
"NETWORK": os.path.join(OUTPUT_DIR, "groqNetwork.txt"),
|
|
"UNKNOWN": os.path.join(OUTPUT_DIR, "groqUnknown.txt"),
|
|
}
|
|
|
|
GROQ_KEY_REGEX = re.compile(r"\bgsk_[A-Za-z0-9_-]{20,}\b")
|
|
MODELS_URL = "https://api.groq.com/openai/v1/models"
|
|
CHAT_URL = "https://api.groq.com/openai/v1/chat/completions"
|
|
CHAT_MODEL_PRIORITY = (
|
|
"llama-3.1-8b-instant",
|
|
"llama-3.3-70b-versatile",
|
|
"llama3-8b-8192",
|
|
"llama3-70b-8192",
|
|
"mixtral-8x7b-32768",
|
|
"gemma2-9b-it",
|
|
)
|
|
NO_BALANCE_MARKERS = (
|
|
"quota",
|
|
"billing",
|
|
"balance",
|
|
"credit",
|
|
"payment",
|
|
"insufficient",
|
|
"depleted",
|
|
)
|
|
|
|
|
|
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, [DETECTOR]):
|
|
key = item["raw"]
|
|
if key and GROQ_KEY_REGEX.fullmatch(key):
|
|
yield key, item["source"], item["finding"]
|
|
|
|
for item in read_plain_keys(plain_files, GROQ_KEY_REGEX):
|
|
key = item["key"]
|
|
if key not in seen_plain:
|
|
seen_plain.add(key)
|
|
yield key, item["source"], {}
|
|
|
|
|
|
def classify_groq_response(response):
|
|
message = request_error_message(response).lower()
|
|
if response.status_code == 401:
|
|
return "DEAD"
|
|
if response.status_code == 403:
|
|
return "RESTRICTED"
|
|
if response.status_code == 429:
|
|
if any(marker in message for marker in NO_BALANCE_MARKERS):
|
|
return "NO_BALANCE"
|
|
return "LIMITED"
|
|
return classify_common_http_status(response.status_code)
|
|
|
|
|
|
def notable_models(payload):
|
|
models = payload.get("data", []) if isinstance(payload, dict) else []
|
|
ids = []
|
|
for item in models:
|
|
if isinstance(item, dict) and item.get("id"):
|
|
ids.append(str(item.get("id")))
|
|
priority = []
|
|
for marker in ("llama", "mixtral", "gemma", "whisper"):
|
|
for model in ids:
|
|
if marker in model.lower() and model not in priority:
|
|
priority.append(model)
|
|
return priority[:20], len(ids), ids
|
|
|
|
|
|
def choose_chat_model(model_ids):
|
|
model_ids = [str(model or "") for model in model_ids if model]
|
|
by_lower = {model.lower(): model for model in model_ids}
|
|
for model in CHAT_MODEL_PRIORITY:
|
|
if model.lower() in by_lower:
|
|
return by_lower[model.lower()]
|
|
for marker in ("llama", "mixtral", "gemma"):
|
|
for model in model_ids:
|
|
lowered = model.lower()
|
|
if marker in lowered and "whisper" not in lowered and "guard" not in lowered:
|
|
return model
|
|
return ""
|
|
|
|
|
|
def probe_chat_completion(key, model, proxy, timeout, debug=False):
|
|
if not model:
|
|
return {"status": "NO_CONTEXT", "message": "no chat-capable model from /models", "model": ""}
|
|
headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
payload = {"model": model, "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1}
|
|
try:
|
|
response = requests.post(CHAT_URL, headers=headers, json=payload, proxies=proxy, timeout=timeout)
|
|
except requests.RequestException as exc:
|
|
return {"status": "NETWORK", "message": str(exc), "model": model}
|
|
|
|
if debug:
|
|
print(f" DEBUG chat ping {model}: HTTP {response.status_code}: {response.text[:500].replace(key, '***REDACTED***')}")
|
|
|
|
if response.status_code == 200:
|
|
return {"status": "GENERATION_OK", "message": "chat completion accepted", "model": model}
|
|
return {
|
|
"status": classify_groq_response(response),
|
|
"http_status": response.status_code,
|
|
"message": request_error_message(response).replace(key, "***REDACTED***"),
|
|
"model": model,
|
|
}
|
|
|
|
|
|
def check_key(key, proxy, timeout, debug=False):
|
|
headers = {"Authorization": f"Bearer {key}", "Accept": "application/json"}
|
|
try:
|
|
response = requests.get(MODELS_URL, headers=headers, proxies=proxy, timeout=timeout)
|
|
except requests.RequestException as exc:
|
|
return {"status": "NETWORK", "message": str(exc)}
|
|
|
|
if debug:
|
|
print(f" DEBUG /models: HTTP {response.status_code}: {response.text[:500].replace(key, '***REDACTED***')}")
|
|
|
|
if response.status_code == 200:
|
|
try:
|
|
payload = response.json()
|
|
except ValueError:
|
|
payload = {}
|
|
models, model_count, model_ids = notable_models(payload)
|
|
chat_model = choose_chat_model(model_ids)
|
|
probe = probe_chat_completion(key, chat_model, proxy, timeout, debug)
|
|
if probe.get("status") != "GENERATION_OK":
|
|
return {
|
|
"status": probe.get("status") or "UNKNOWN",
|
|
"message": probe.get("message", ""),
|
|
"model_count": model_count,
|
|
"models": models,
|
|
"llm_probe_status": probe.get("status"),
|
|
"llm_probe_model": probe.get("model", chat_model),
|
|
"llm_probe_http_status": probe.get("http_status"),
|
|
}
|
|
return {
|
|
"status": "VALID",
|
|
"message": f"chat ping ok; model={chat_model}; models={model_count}",
|
|
"model_count": model_count,
|
|
"models": models,
|
|
"llm_probe_status": probe.get("status"),
|
|
"llm_probe_model": chat_model,
|
|
}
|
|
|
|
return {
|
|
"status": classify_groq_response(response),
|
|
"http_status": response.status_code,
|
|
"message": request_error_message(response).replace(key, "***REDACTED***"),
|
|
}
|
|
|
|
|
|
def write_result(key, result, source, finding):
|
|
status = result.get("status") or "UNKNOWN"
|
|
write_keycheck_event(SERVICE, RESULTS_FILE, key, result, source, finding, DETECTOR)
|
|
extra = ",".join(result.get("models") or [])[:500] if status == "VALID" else source
|
|
commit_status_transaction(
|
|
CHECKED_FILE, STATUS_FILES, key, status, result.get("message", ""), extra,
|
|
)
|
|
record_validation_result(SERVICE, key, result, source, finding, DETECTOR)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Groq 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=15)
|
|
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-restricted", action="store_true")
|
|
parser.add_argument("--retry-no-balance", action="store_true")
|
|
parser.add_argument("--retry-valid", action="store_true")
|
|
parser.add_argument("--recheck-all", action="store_true")
|
|
parser.add_argument("--debug", 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_restricted:
|
|
retry_statuses.add("RESTRICTED")
|
|
if args.retry_no_balance:
|
|
retry_statuses.add("NO_BALANCE")
|
|
if args.retry_valid:
|
|
retry_statuses.add("VALID")
|
|
|
|
print("--- Groq key checker ---")
|
|
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=DETECTOR):
|
|
skipped += 1
|
|
continue
|
|
if args.max_keys and processed >= args.max_keys:
|
|
break
|
|
processed += 1
|
|
print(f"\n[{processed}] Groq candidate {mask_secret(key)} from {source}")
|
|
proxy = next(proxy_cycler) if proxy_cycler else None
|
|
result = check_key(key, proxy, args.timeout, args.debug)
|
|
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()
|