import sys sys.dont_write_bytecode = True import requests import json import os import argparse from itertools import cycle from datetime import datetime, timezone sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) try: sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stderr.reconfigure(encoding='utf-8', errors='replace') except Exception: pass from keycheck_common import ( acquire_file_lock, append_checked, append_jsonl, commit_status_transaction, default_input_file, default_proxy_file, env_int, ensure_output_files as ensure_private_output_files, finding_detector_secret_hash, iter_findings, iter_bounded_text_lines, keycheck_input_mode, load_known_statuses, load_checked_statuses, mask_secret, now_iso, physical_jsonl_segments, private_append_writer, private_atomic_writer, reconcile_keycheck_jsonl_segments, record_validation_result, recover_status_transaction, release_file_lock, repair_keycheck_jsonl_tail, require_provider_authority, rotate_jsonl_if_needed, service_output_dir, should_skip_key, sha256_text, write_keycheck_event, ) from runtime_security import reject_reparse_components, require_private_file # --- Конфигурация --- SERVICE = "openrouter" OUTPUT_DIR = os.getenv("KEYCHECK_OUTPUT_DIR") or service_output_dir(SERVICE) INPUT_FILE = os.getenv("KEYCHECK_INPUT_FILE") or default_input_file() ALIVE_FILE = os.path.join(OUTPUT_DIR, "openrouterAlive.txt") DEAD_FILE = os.path.join(OUTPUT_DIR, "openrouterDead.txt") LIMITED_FILE = os.path.join(OUTPUT_DIR, "openrouterLimited.txt") NO_BALANCE_FILE = os.path.join(OUTPUT_DIR, "openrouterNoBalance.txt") NETWORK_FILE = os.path.join(OUTPUT_DIR, "openrouterNetwork.txt") UNKNOWN_FILE = os.path.join(OUTPUT_DIR, "openrouterUnknown.txt") CHECKED_FILE = os.path.join(OUTPUT_DIR, "openrouterChecked.txt") RESULTS_FILE = os.path.join(OUTPUT_DIR, "openrouterResults.jsonl") PROXY_FILE = os.getenv("KEYCHECK_PROXY_FILE") or default_proxy_file() STATUS_FILES = { "VALID": ALIVE_FILE, "NO_BALANCE": NO_BALANCE_FILE, "DEAD": DEAD_FILE, "LIMITED": LIMITED_FILE, "NETWORK": NETWORK_FILE, "UNKNOWN": UNKNOWN_FILE, } CREDITS_URL = "https://openrouter.ai/api/v1/credits" # --- Вспомогательные функции --- def load_set_from_file(filepath): """Загружает ключи из файла в set для быстрой проверки.""" if not os.path.exists(filepath): return set() return {line.strip().split(':')[0] for line in iter_bounded_text_lines(filepath) if line.strip()} def ensure_output_files(): ensure_private_output_files((CHECKED_FILE, RESULTS_FILE, *STATUS_FILES.values())) recover_status_transaction(CHECKED_FILE, STATUS_FILES) def legacy_status_key(line): value = line.strip() if not value: return None if '\t' in value: return value.split('\t', 1)[0].strip() return value.split(':', 1)[0].strip() def load_openrouter_keys(path): if keycheck_input_mode() == 'postgres': return set() if not os.path.exists(path): return set() return {key for key in (legacy_status_key(line) for line in iter_bounded_text_lines(path)) if key} def iter_plain_openrouter_keys(paths): if keycheck_input_mode() == 'postgres': return seen = set() items = [] for path in paths or []: if not path or not os.path.exists(path): continue for line in iter_bounded_text_lines(path): key = legacy_status_key(line) if not key or key in seen: continue seen.add(key) items.append({'raw': key, 'source': path, 'finding': {}}) for item in items: yield item def retry_plain_files(args): if keycheck_input_mode() == 'postgres': return [] files = list(args.plain or []) if args.recheck_all: files.extend(STATUS_FILES.values()) else: if args.retry_valid: files.append(ALIVE_FILE) if args.retry_no_balance: files.append(NO_BALANCE_FILE) if args.retry_limited: files.append(LIMITED_FILE) if args.retry_network: files.append(NETWORK_FILE) if args.retry_unknown: files.append(UNKNOWN_FILE) out = [] seen = set() for path in files: if path and path not in seen: seen.add(path) out.append(path) return out def migrate_legacy_checked(): if keycheck_input_mode() == 'postgres': return checked = load_checked_statuses(CHECKED_FILE) for key in sorted(load_openrouter_keys(ALIVE_FILE)): if key not in checked: write_keycheck_event(SERVICE, RESULTS_FILE, key, {'status': 'VALID', 'message': 'legacy alive status migration'}, 'legacy:openrouterAlive.txt', {}, 'OpenRouter', 'legacy_status') append_checked(CHECKED_FILE, key, 'VALID') checked[key] = 'VALID' for key in sorted(load_openrouter_keys(DEAD_FILE)): if key not in checked: write_keycheck_event(SERVICE, RESULTS_FILE, key, {'status': 'DEAD', 'message': 'legacy dead status migration'}, 'legacy:openrouterDead.txt', {}, 'OpenRouter', 'legacy_status') append_checked(CHECKED_FILE, key, 'DEAD') checked[key] = 'DEAD' def _legacy_alive_checked_at(path): details = os.stat(path, follow_symlinks=False) return datetime.fromtimestamp(details.st_mtime, timezone.utc).isoformat(timespec='seconds') def _legacy_balance_event_payload(key, balance, checked_at): balance_text = f'{balance:.6f}' key_hash = sha256_text(key) event_id = sha256_text('|'.join([ SERVICE, 'legacy_status', 'openrouterAlive.txt', key_hash, 'NO_BALANCE', balance_text, ])) return { 'key_masked': mask_secret(key), 'key_hash': key_hash, 'secret_hash': key_hash, 'detector_secret_hash': finding_detector_secret_hash({}), 'finding_uid': '', 'detector': 'OpenRouter', 'source': 'legacy:openrouterAlive.txt', 'finding': {}, 'checked_at': checked_at, 'result_source': 'legacy_status', 'status': 'NO_BALANCE', 'message': f'credits={balance_text}', 'event_id': event_id, } def _publish_legacy_no_balance(moved): lock_path = f'{NO_BALANCE_FILE}.lock' lock = acquire_file_lock(lock_path, timeout_sec=30) try: require_private_file(NO_BALANCE_FILE) existing = load_openrouter_keys(NO_BALANCE_FILE) with private_append_writer(NO_BALANCE_FILE) as handle: for key, balance in moved: if key in existing: continue balance_text = f'{balance:.6f}' handle.write(f'{key}\tNO_BALANCE\tcredits={balance_text}\tmigrated_from_alive\n') existing.add(key) finally: release_file_lock(lock, lock_path) def _existing_legacy_event_ids(expected): found = set() max_line_bytes = max(1024, env_int('KEYCHECK_INPUT_MAX_LINE_BYTES', 16 * 1024 * 1024)) max_file_bytes = max( max_line_bytes, max(1, env_int('KEYCHECK_INPUT_LIST_MAX_BYTES', 32 * 1024 * 1024)), max(0, env_int('KEYCHECK_RESULTS_MAX_MB', 32)) * 1024 * 1024 + max_line_bytes, ) paths = [path for _, path in physical_jsonl_segments(RESULTS_FILE)] if os.path.isfile(RESULTS_FILE): paths.append(os.path.abspath(RESULTS_FILE)) for path in paths: reject_reparse_components(path) if os.path.getsize(path) > max_file_bytes: raise RuntimeError(f'OpenRouter result file exceeds the bounded migration scan size: {path}') with open(path, 'rb') as handle: while True: raw_line = handle.readline(max_line_bytes + 1) if not raw_line: break if len(raw_line) > max_line_bytes: raise RuntimeError(f'OpenRouter result line exceeds the bounded migration scan size: {path}') if not raw_line.endswith(b'\n'): raise RuntimeError(f'torn OpenRouter result line during legacy migration: {path}') try: payload = json.loads(raw_line.decode('utf-8', errors='replace')) except (TypeError, ValueError): continue event_id = str(payload.get('event_id') or '') if isinstance(payload, dict) else '' if event_id not in expected: continue wanted = expected[event_id] for field in ('key_hash', 'status', 'source', 'result_source'): if str(payload.get(field) or '') != str(wanted.get(field) or ''): raise RuntimeError(f'conflicting OpenRouter legacy migration event: {event_id}') found.add(event_id) return found def _publish_legacy_balance_events(moved, checked_at): payloads = [_legacy_balance_event_payload(key, balance, checked_at) for key, balance in moved] expected = {payload['event_id']: payload for payload in payloads} lock_path = f'{RESULTS_FILE}.lock' lock = acquire_file_lock(lock_path, timeout_sec=30) try: require_private_file(RESULTS_FILE) repair_keycheck_jsonl_tail(RESULTS_FILE) reconcile_keycheck_jsonl_segments(RESULTS_FILE) existing = _existing_legacy_event_ids(expected) max_bytes = max(0, env_int('KEYCHECK_RESULTS_MAX_MB', 32)) * 1024 * 1024 for payload in payloads: event_id = payload['event_id'] if event_id in existing: continue rotate_jsonl_if_needed(RESULTS_FILE, max_bytes) with private_append_writer(RESULTS_FILE) as handle: handle.write(json.dumps(payload, ensure_ascii=False, default=str) + '\n') existing.add(event_id) finally: release_file_lock(lock, lock_path) def _publish_legacy_checked(moved, checked_at): lock_path = f'{CHECKED_FILE}.lock' lock = acquire_file_lock(lock_path, timeout_sec=30) try: require_private_file(CHECKED_FILE) existing = set() for line in iter_bounded_text_lines(CHECKED_FILE): parts = line.rstrip('\r\n').split('\t') if len(parts) >= 2: existing.add((parts[0], parts[1])) with private_append_writer(CHECKED_FILE) as handle: for key, _ in moved: identity = (key, 'NO_BALANCE') if identity in existing: continue handle.write(f'{key}\tNO_BALANCE\t{checked_at}\n') existing.add(identity) finally: release_file_lock(lock, lock_path) def _rewrite_legacy_alive(keep): with private_atomic_writer(ALIVE_FILE, binary=True, suffix='.legacy.tmp') as handle: for line in keep: handle.write(line.encode('utf-8')) def migrate_legacy_alive_balances(): if keycheck_input_mode() == 'postgres': return lock_path = f'{ALIVE_FILE}.lock' lock = acquire_file_lock(lock_path, timeout_sec=30) try: if not os.path.exists(ALIVE_FILE): return require_private_file(ALIVE_FILE) checked_at = _legacy_alive_checked_at(ALIVE_FILE) keep = [] moved = [] seen = set() for line in iter_bounded_text_lines(ALIVE_FILE): text = line.strip() if not text: keep.append(line) continue key = legacy_status_key(text) balance = None if ':' in text and '\t' not in text: try: balance = float(text.rsplit(':', 1)[1]) except ValueError: balance = None if key and balance is not None and balance <= 0: if key not in seen: moved.append((key, balance)) seen.add(key) else: keep.append(line) if not moved: return _publish_legacy_no_balance(moved) _publish_legacy_balance_events(moved, checked_at) _publish_legacy_checked(moved, checked_at) _rewrite_legacy_alive(keep) finally: release_file_lock(lock, lock_path) def load_proxies(proxy_file=None): """Загружает и подготавливает прокси.""" proxy_file = proxy_file or PROXY_FILE if not os.path.exists(proxy_file): print("ℹ️ Файл proxy.txt не найден, запросы будут идти напрямую.") return None proxies = [] with open(proxy_file, 'r') as f: for line in f: line = line.strip() if not line: continue try: ip, port, login, password = line.split(':') proxy_url = f"http://{login}:{password}@{ip}:{port}" proxies.append({"http": proxy_url, "https": proxy_url}) except ValueError: print(f"⚠️ Неверный формат прокси: '{line}'. Пропускаем.") if not proxies: print("⚠️ Файл proxy.txt пуст. Запросы будут идти напрямую.") return None print(f"✅ Загружено {len(proxies)} прокси.") return cycle(proxies) def write_result(key, result, source_line, finding=None, previous_status=None): status = result.get('status') or 'UNKNOWN' credits = result.get('remaining_credits') extra = f"credits={credits:.6f}" if isinstance(credits, (int, float)) else source_line checked_at = now_iso() result = {**result, 'checked_at': result.get('checked_at') or checked_at} write_keycheck_event(SERVICE, RESULTS_FILE, key, result, source_line, finding, 'OpenRouter') commit_status_transaction( CHECKED_FILE, STATUS_FILES, key, status, result.get('message', ''), extra, ) record_validation_result(SERVICE, key, result, source_line, finding, 'OpenRouter') # --- Функция проверки --- def check_openrouter_key(key, proxy): """Проверяет один ключ OpenRouter и возвращает normalized result.""" headers = {"Authorization": f"Bearer {key}"} try: resp = requests.get(CREDITS_URL, headers=headers, proxies=proxy, timeout=15) except requests.exceptions.RequestException as e: return {'status': 'NETWORK', 'message': str(e)} if resp.status_code == 200: try: data = resp.json().get("data", {}) total = float(data.get("total_credits", 0.0) or 0.0) used = float(data.get("total_usage", 0.0) or 0.0) except (TypeError, ValueError, json.JSONDecodeError) as exc: return {'status': 'UNKNOWN', 'http_status': resp.status_code, 'message': f'invalid credits response: {exc}'} remaining = total - used if remaining > 0: return {'status': 'VALID', 'remaining_credits': remaining, 'message': f'credits={remaining:.6f}'} return {'status': 'NO_BALANCE', 'remaining_credits': remaining, 'message': f'credits={remaining:.6f}'} if resp.status_code in (401, 403): return {'status': 'DEAD', 'http_status': resp.status_code, 'message': resp.text[:1000]} if resp.status_code == 429: return {'status': 'LIMITED', 'http_status': resp.status_code, 'message': resp.text[:1000]} if 500 <= resp.status_code <= 599: return {'status': 'NETWORK', 'http_status': resp.status_code, 'message': resp.text[:1000]} return {'status': 'UNKNOWN', 'http_status': resp.status_code, 'message': resp.text[:1000]} def parse_args(): parser = argparse.ArgumentParser(description='OpenRouter 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('--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('--retry-valid', 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_output_files() migrate_legacy_alive_balances() migrate_legacy_checked() print("--- 🚀 Запуск чекера ключей OpenRouter 🚀 ---") proxy_cycler = load_proxies(args.proxy_file) checked_statuses = load_checked_statuses(CHECKED_FILE) known_statuses = load_known_statuses(CHECKED_FILE, STATUS_FILES) known_keys = set(known_statuses) for path in STATUS_FILES.values(): known_keys.update(load_openrouter_keys(path)) 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_BALANCE') if args.retry_valid: retry_statuses.add('VALID') print(f"📖 Загружено: {len(load_openrouter_keys(ALIVE_FILE))} живых ключей, {len(known_keys)} классифицированных ключей.") if keycheck_input_mode() == 'jsonl' and not os.path.exists(args.input): print(f"❌ Файл с секретами {args.input} не найден. Завершение.") return processed = 0 def candidates(): for item in iter_findings(args.input, ["OpenRouter"]): key = item.get("raw") or "" if key: yield item seen = set() for item in iter_plain_openrouter_keys(retry_plain_files(args)): key = item.get("raw") or "" if key and key not in seen: seen.add(key) yield item for item in candidates(): key = item.get("raw") or "" if not key: continue source = item.get("source") or args.input finding = item.get("finding") or {} if should_skip_key( key, checked_statuses, known_keys, args, retry_statuses, service=SERVICE, source=source, finding=finding, detector='OpenRouter', known_statuses=known_statuses, ): continue if args.max_keys and processed >= args.max_keys: break processed += 1 print(f"\n[{processed}] 🎯 Новый кандидат: {key[:8]}...{key[-4:]} from {source}") current_proxy = next(proxy_cycler) if proxy_cycler else None result = check_openrouter_key(key, current_proxy) print(f" STATUS: {result.get('status')} | {str(result.get('message', ''))[:200]}") previous_status = known_statuses.get(key) or checked_statuses.get(key) write_result(key, result, source, finding, previous_status) known_keys.add(key) checked_statuses[key] = result.get('status') print("\n--- ✅ Проверка завершена. ---") if __name__ == "__main__": main()