├── requirements.txt ├── .gitignore ├── LICENSE ├── README.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/**/ 3 | !*.* 4 | !LICENSE 5 | 6 | *.txt 7 | !requirements.txt 8 | scan_results -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## KKC 2 | 3 | - Install Python 4 | - `pip install -r requirements.txt` 5 | - `python main.py keys.txt` (assuming you have keys.txt with the keys in the same folder) 6 | 7 | If you want to see the key scanning progress, run the program with `-v`, like `python main.py -v keys.txt`. 8 | 9 | You can also change the amount of simultaneous requests done by the script by using the `-r` or `--requests` option. For example, to limit the requests to 10 at a time, use `python main.py -r 10 keys.txt`. The default is `20`. 10 | 11 | ### Output 12 | The checker will output keys both to the console and into the files. The `scan_results` folder will contain files with the keys for the model, and all over-quota keys will go to the `scan_results/over_quota.txt` file. 13 | 14 | ### Features 15 | - Asynchronous - multiple key checks are done at the same time 16 | - Ratelimits - allows to see if a key is a trial one, or has higher than default ratelimits, which usually means better quota 17 | - Organizations - fetches the list of all organizations (including their names) a key can be used with, and checks each organization's status separately. If an organization is not a default one, it usually means better quota 18 | 19 | ### Changelog 20 | - 2023/09/07 21 | - Improved organization handling, now the key checker will try the key with all of the organizations assigned to it, and will show the status of each organization separately. 22 | - When a ratelimit wasn't received in the completion response, it'll properly show up as unknown instead of 0. -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import re 4 | import asyncio 5 | from typing import List 6 | import aiohttp 7 | 8 | BASE_API = "https://api.openai.com/v1" 9 | RATE_LIMIT_PER_MODEL = {"gpt-3.5-turbo": 3500, "gpt-4": 200, "gpt-4-32k": 10} 10 | oai_key_regex = re.compile(r"(sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})") 11 | 12 | # utils 13 | def get_headers(key: str, org_id: str = None): 14 | headers = {"Authorization": f"Bearer {key}"} 15 | if org_id: 16 | headers["OpenAI-Organization"] = org_id 17 | return headers 18 | 19 | class Key: 20 | def __init__(self, key_string: str, models: List[str] = []): 21 | self.key_string = key_string 22 | self.dead = False 23 | self.working = False 24 | self.trial_status = False 25 | self.over_quota = False 26 | self.org_name = "" 27 | self.org_id = "" 28 | self.org_default = False 29 | self.models = models 30 | self.ratelimit = 0 31 | 32 | def top_model(self) -> str: 33 | for model in reversed(RATE_LIMIT_PER_MODEL.keys()): 34 | if model in self.models: 35 | return model 36 | return "" 37 | 38 | class KeyScanner: 39 | def __init__(self, keys: List[str], verbose: bool, max_requests = 20): 40 | self.keys = [match.group(1) for match in (oai_key_regex.search(key) for key in keys) if match] 41 | self.verbose = verbose 42 | self.file_handles = {model: open(f"scan_results/{model}.txt", "w") for model in RATE_LIMIT_PER_MODEL.keys()} 43 | self.file_handles["over_quota"] = open("scan_results/over_quota.txt", "w") 44 | self.sem = asyncio.Semaphore(max_requests) 45 | 46 | print(f"Total unique key count: {len(keys)}, starting the scan...") 47 | 48 | async def scan(self): 49 | tasks = [self.check_key(key) for key in self.keys] 50 | return await asyncio.gather(*tasks) 51 | 52 | async def check_key(self, key: str): 53 | result = [] 54 | async with self.sem: 55 | if self.verbose: 56 | print("Checking key", key) 57 | 58 | orgs = await self.get_orgs(key) 59 | if not orgs: 60 | return result 61 | 62 | for org in orgs: 63 | is_default_org = org["is_default"] 64 | if not is_default_org and self.verbose: 65 | print(f"Checking alternative org {org['name']} for {key}") 66 | 67 | models = await self.get_models(key, org["id"]) 68 | # Not sure if this can happen, but just to be safe 69 | if not models: 70 | continue 71 | 72 | status = Key(key, models=models) 73 | status.org_default = is_default_org 74 | status.org_name = org["name"] 75 | status.org_id = org["id"] 76 | 77 | top_model_name = status.top_model() 78 | await self.try_completion(status, top_model_name) 79 | 80 | if status.working or status.over_quota: 81 | self.write_key_to_file(status, top_model_name) 82 | result.append(status) 83 | if self.verbose: 84 | print(f"Good key {key} with model {top_model_name}") 85 | 86 | return result 87 | 88 | async def get_models(self, key: str, org_id: str) -> List[str]: 89 | async with aiohttp.ClientSession() as session: 90 | async with session.get(f"{BASE_API}/models", headers=get_headers(key, org_id)) as resp: 91 | if resp.status != 200: 92 | return [] 93 | data = await resp.json() 94 | result = [model["id"] for model in data["data"] if model["id"] in RATE_LIMIT_PER_MODEL] 95 | result.sort(key=lambda x: list(RATE_LIMIT_PER_MODEL.keys()).index(x)) 96 | return result 97 | 98 | async def try_completion(self, status: Key, model: str): 99 | async with aiohttp.ClientSession() as session: 100 | req_data = {"model": model, "messages": [{"role": "user", "content": ""}], "max_tokens": -1} 101 | async with session.post(f"{BASE_API}/chat/completions", headers=get_headers(status.key_string, status.org_id), json=req_data) as resp: 102 | data = await resp.json() 103 | if resp.status == 401: 104 | # Just an invalid key 105 | return 106 | 107 | error_type = data.get("error", {}).get("type", "") 108 | if error_type in ["access_terminated"]: 109 | # Banned 110 | return 111 | # A billing terminated account CAN get restored! 112 | # https://gitgud.io/khanon/oai-reverse-proxy/-/merge_requests/45/diffs?commit_id=a06de8055ad6a3044981a9f1c8a5ca63a44ea1d9 113 | elif error_type in ["insufficient_quota", "billing_not_active"]: 114 | # Over quota 115 | status.over_quota = True 116 | return 117 | 118 | # Get the ratelimit for the top model 119 | ratelimit = int(resp.headers.get("x-ratelimit-limit-requests", "-1")) 120 | status.ratelimit = ratelimit 121 | # This really only gets triggered for turbo 122 | if ratelimit < RATE_LIMIT_PER_MODEL[model]: 123 | status.trial_status = True 124 | 125 | # If a key is overused by others but valid, it might be 126 | # ratelimited when we're doing our request 127 | ratelimited = resp.status == 429 128 | if (resp.status == 400 and error_type == "invalid_request_error") or ratelimited: 129 | status.working = True 130 | return 131 | 132 | async def get_orgs(self, key: str): 133 | """ 134 | Undocumented OpenAI API, "data" key is an array of org entries: 135 | { 136 | "object": "organization", 137 | "id": "org-", 138 | "created": 1685299576, 139 | "title": "", 140 | "name": "", 141 | "description": null, 142 | "personal": false, 143 | "is_default": true, 144 | "role": "owner" 145 | } 146 | """ 147 | url = "https://api.openai.com/v1/organizations" 148 | async with aiohttp.ClientSession() as session: 149 | async with session.get(url, headers=get_headers(key)) as resp: 150 | if resp.status != 200: 151 | return [] 152 | data = await resp.json() 153 | return data["data"] 154 | 155 | def write_key_to_file(self, status: Key, top_model_name: str): 156 | outfile = self.file_handles["over_quota" if status.over_quota else top_model_name] 157 | output = status.key_string 158 | addons = [] 159 | if not status.org_name.startswith("user-"): 160 | if status.org_default: 161 | addons.append(f"org '{status.org_name}'") 162 | else: 163 | addons.append(f"alternate org '{status.org_name}' with id '{status.org_id}'") 164 | 165 | if status.trial_status: 166 | addons.append("trial") 167 | if status.over_quota and "gpt-4" in top_model_name: 168 | addons.append(f"has {top_model_name}") 169 | if addons: 170 | output += " (" + ", ".join(addons) + ")" 171 | outfile.write(output + "\n") 172 | outfile.flush() 173 | 174 | def main(): 175 | parser = argparse.ArgumentParser(description="KKC - OpenAI key checker") 176 | parser.add_argument("file", help="Input file containing the keys") 177 | parser.add_argument("-v", "--verbose", action="store_true", help="Verbose scanning") 178 | parser.add_argument("-r", "--requests", type=int, default=20, help="Max number of requests to make at once") 179 | 180 | args = parser.parse_args() 181 | 182 | with open(args.file, "r") as f: 183 | keys = f.read().splitlines() 184 | 185 | keys = list(set(keys)) 186 | 187 | if not os.path.exists("scan_results"): 188 | os.makedirs("scan_results") 189 | 190 | scanner = KeyScanner(keys, args.verbose, args.requests) 191 | # Each scan returns a list of 1 or more statuses (alternate orgs) 192 | good_keys = [] 193 | for key_result in asyncio.run(scanner.scan()): 194 | good_keys.extend(key_result) 195 | 196 | # Initialize counters 197 | model_key_counts = {model: 0 for model in RATE_LIMIT_PER_MODEL.keys()} 198 | 199 | for key in good_keys: 200 | if not key or key.over_quota: continue 201 | top_model = key.top_model() 202 | model_key_counts[top_model] += 1 203 | print("---") 204 | print(f"{key.key_string}") 205 | for model in key.models: 206 | if model == top_model: 207 | ratelimit_text = str(key.ratelimit) if key.ratelimit >= 0 else "unknown" 208 | print(f" - {model} (RPM: {ratelimit_text})") 209 | else: 210 | print(f" - {model}") 211 | if key.trial_status: 212 | print(" - !trial key!") 213 | if not key.org_name.startswith("user-"): 214 | if key.org_default: 215 | print(f"Main org: {key.org_name}") 216 | else: 217 | print(f"Alternate org: {key.org_name} (id {key.org_id})") 218 | 219 | print("---\n") 220 | 221 | # Calculate total good keys 222 | total_good_keys = sum(model_key_counts.values()) 223 | 224 | # Print total good keys and per model counts 225 | print(f"\nTotal good keys: {total_good_keys}") 226 | for model, count in model_key_counts.items(): 227 | if count > 0: 228 | print(f"Number of good keys for {model}: {count}") 229 | 230 | for file in scanner.file_handles.values(): 231 | file.close() 232 | 233 | if __name__ == "__main__": 234 | main() 235 | --------------------------------------------------------------------------------