{"repo": "chenghengli/claude-free-installer", "files": {"claude-free-audit.py": {"1d34ec7a89dca06873eed97d50c8f90e968e2ec5": {"t": 1785674655.7196822, "n": 48508, "h": "e6c98e21327f13c2", "c": "#!/usr/bin/env python3\n\"\"\"claude-free-audit — generated single-file bundle of the `claude_free` package.\n\nDO NOT EDIT THIS FILE BY HAND.\n\nSource of truth: src/claude_free/ in https://github.com/ChenghengLi/claude-free-installer\nRegenerate: python tools/build.py\n\nThis file is what `claude-free update` and the installers fetch. It has zero\nexternal dependencies — only the Python standard library.\n\nUsage:\n claude-free-audit.py audit [flags...]\n claude-free-audit.py calibrate [flags...]\n claude-free-audit.py --help\n\nSee docs/COMMANDS.md and docs/AUDIT.md for details.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport collections\nimport json\nimport math\nimport os\nimport re\nimport statistics\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom pathlib import Path\nfrom typing import Optional, Protocol\n\n# ============================================================\n# === __init__.py\n# ============================================================\n\n__version__ = \"0.3.0\"\n\n# Last-refreshed marker for the BENCHMARKS table — keep in sync with\n# benchmarks.py. Used by the build script to stamp the generated file.\nBENCHMARKS_REFRESHED = \"2026-05-02\"\n\n# ============================================================\n# === colors.py\n# ============================================================\n\ndef colors() -> dict[str, str]:\n \"\"\"Return a dict of ANSI escape codes, or empty strings if colour is off.\"\"\"\n if not sys.stdout.isatty() or os.environ.get(\"NO_COLOR\"):\n return {k: \"\" for k in (\"B\", \"D\", \"G\", \"Y\", \"R\", \"N\")}\n return {\n \"B\": \"\\033[1m\", # bold\n \"D\": \"\\033[2m\", # dim\n \"G\": \"\\033[0;32m\", # green\n \"Y\": \"\\033[0;33m\", # yellow\n \"R\": \"\\033[0;31m\", # red\n \"N\": \"\\033[0m\", # reset\n }\n\n# ============================================================\n# === rate_limit.py\n# ============================================================\n\nclass RateLimiter:\n \"\"\"Sliding-window rate limiter.\n\n Free NVIDIA NIM is ~40 req/min/key; we default callers to 30/min to\n leave headroom for the proxy and other concurrent use. Call .wait()\n before every outbound request — it sleeps as needed and returns the\n number of seconds slept (for telemetry).\n \"\"\"\n\n def __init__(self, rate_per_min: float):\n self.rate = max(1.0, float(rate_per_min))\n self.window = 60.0\n self.timestamps: collections.deque[float] = collections.deque()\n\n def _evict(self, now: float) -> None:\n cutoff = now - self.window\n while self.timestamps and self.timestamps[0] < cutoff:\n self.timestamps.popleft()\n\n def wait(self) -> float:\n now = time.monotonic()\n self._evict(now)\n slept = 0.0\n while len(self.timestamps) >= self.rate:\n wait_for = (self.timestamps[0] + self.window) - now + 0.05\n if wait_for > 0:\n time.sleep(wait_for)\n slept += wait_for\n now = time.monotonic()\n self._evict(now)\n self.timestamps.append(now)\n return slept\n\n# ============================================================\n# === env.py\n# ============================================================\n\ndef env_path() -> Path:\n \"\"\"Default location of the free-claude-code .env file.\"\"\"\n return Path.home() / \"free-claude-code\" / \".env\"\n\ndef read_env_key(path: Path, key: str) -> Optional[str]:\n \"\"\"Return the (unquoted, comment-stripped) value of `key` in `path`, or None.\"\"\"\n if not path.exists():\n return None\n pat = re.compile(rf\"^\\s*{re.escape(key)}\\s*=\\s*(.*?)\\s*$\")\n for line in path.read_text(encoding=\"utf-8\", errors=\"replace\").splitlines():\n m = pat.match(line)\n if not m:\n continue\n v = m.group(1)\n v = v.split(\"#\", 1)[0].strip()\n if (v.startswith('\"') and v.endswith('\"')) or (v.startswith(\"'\") and v.endswith(\"'\")):\n v = v[1:-1]\n return v\n return None\n\ndef write_env_key(path: Path, key: str, value: str) -> None:\n \"\"\"In-place set `key=\"value\"` in `path`. Creates the file if missing.\"\"\"\n line = f'{key}=\"{value}\"'\n if not path.exists():\n path.write_text(line + \"\\n\", encoding=\"utf-8\")\n return\n txt = path.read_text(encoding=\"utf-8\")\n pat = re.compile(rf\"(?m)^\\s*{re.escape(key)}\\s*=.*$\")\n if pat.search(txt):\n txt = pat.sub(line, txt)\n else:\n if not txt.endswith(\"\\n\"):\n txt += \"\\n\"\n txt += line + \"\\n\"\n path.write_text(txt, encoding=\"utf-8\")\n\ndef resolve_api_key(provider: str = \"nvidia-nim\") -> Optional[str]:\n \"\"\"Look up the API key for `provider` from env vars, then the .env file.\n\n nvidia-nim -> $NVIDIA_NIM_API_KEY > $NVIDIA_API_KEY > .env NVIDIA_NIM_API_KEY\n openrouter -> $OPENROUTER_API_KEY > .env OPENROUTER_API_KEY\n \"\"\"\n if provider == \"nvidia-nim\":\n for var in (\"NVIDIA_NIM_API_KEY\", \"NVIDIA_API_KEY\"):\n v = os.environ.get(var)\n if v:\n return v\n return read_env_key(env_path(), \"NVIDIA_NIM_API_KEY\")\n if provider == \"openrouter\":\n v = os.environ.get(\"OPENROUTER_API_KEY\")\n if v:\n return v\n return read_env_key(env_path(), \"OPENROUTER_API_KEY\")\n return None\n\n# ============================================================\n# === benchmarks.py\n# ============================================================\n\n# Curated shortlist of code/chat-capable models worth ranking by default.\n# Anything not present in the user's account is silently skipped.\n# Order = SWE-bench Verified ranking on NVIDIA NIM (May 2026 snapshot).\nCURATED = [\n # Frontier (>= 76 SWE-bench Verified)\n \"deepseek-ai/deepseek-v4-pro\", # 80.6\n \"minimaxai/minimax-m2.5\", # 80.2\n \"moonshotai/kimi-k2.6\", # 80.2\n \"deepseek-ai/deepseek-v4-flash\", # 79.0\n \"minimaxai/minimax-m2.7\", # 78.0\n \"z-ai/glm-5.1\", # 78.0 (estimate)\n \"z-ai/glm5\", # 77.8\n \"nvidia/llama-3.3-nemotron-super-49b-v1.5\", # 76.5 (estimate)\n \"qwen/qwen3.5-397b-a17b\", # 76.4\n \"nvidia/llama-3.3-nemotron-super-49b-v1\", # 76.4\n # Strong (60-75)\n \"moonshotai/kimi-k2-instruct-0905\", # 73.0 (estimate)\n \"mistralai/devstral-2-123b-instruct-2512\", # 72.2\n \"moonshotai/kimi-k2-thinking\", # 71.0 (estimate)\n \"qwen/qwen3-coder-480b-a35b-instruct\", # 70.0\n \"qwen/qwen3.5-122b-a10b\", # 70.0 (estimate)\n \"z-ai/glm4.7\", # 70.0 (estimate)\n \"nvidia/llama-3.1-nemotron-ultra-253b-v1\", # 68.0 (estimate)\n \"deepseek-ai/deepseek-v3.2\", # 67.8\n \"deepseek-ai/deepseek-v3.1-terminus\", # 62.0 (estimate)\n \"nvidia/nemotron-3-super-120b-a12b\", # 60.5\n # Mid (35-60)\n \"mistralai/mistral-large-3-675b-instruct-2512\", # 58.0 (estimate)\n \"meta/llama-4-maverick-17b-128e-instruct\", # 55.0 (estimate)\n \"qwen/qwen2.5-72b-instruct\", # 50.0 (LCB)\n \"qwen/qwen3-next-80b-a3b-thinking\", # 50.0 (estimate)\n \"qwen/qwen2.5-coder-32b-instruct\", # 50.0\n \"deepseek-ai/deepseek-r1\", # 49.2\n \"mistralai/mistral-medium-3.5-128b\", # 45.0 (estimate)\n # Lower / fast tier (kept for comparison)\n \"openai/gpt-oss-120b\", # 30.0\n \"meta/llama-3.3-70b-instruct\", # 30.0 (estimate)\n \"openai/gpt-oss-20b\", # 15.0 (estimate)\n \"mistralai/mixtral-8x22b-instruct-v0.1\", # 22.0\n \"meta/llama-3.1-8b-instruct\", # 12.0\n]\n\n# Last refreshed: 2026-05-02. Vendor model cards / official leaderboards.\nBENCHMARKS: dict[str, dict] = {\n # ---- frontier (>= 76) ----\n \"deepseek-ai/deepseek-v4-pro\": {\"swebench\": 80.6, \"code_score\": 80.6, \"src\": \"swebench\"},\n \"minimaxai/minimax-m2.5\": {\"swebench\": 80.2, \"code_score\": 80.2, \"src\": \"swebench\"},\n \"moonshotai/kimi-k2.6\": {\"swebench\": 80.2, \"code_score\": 80.2, \"src\": \"swebench\"},\n \"deepseek-ai/deepseek-v4-flash\": {\"swebench\": 79.0, \"code_score\": 79.0, \"src\": \"swebench\"},\n \"minimaxai/minimax-m2.7\": {\"swebench\": 78.0, \"code_score\": 78.0, \"src\": \"swebench\"},\n \"z-ai/glm-5.1\": {\"swebench\": 78.0, \"code_score\": 78.0, \"src\": \"estimate\"},\n \"z-ai/glm5\": {\"swebench\": 77.8, \"code_score\": 77.8, \"src\": \"swebench\"},\n \"nvidia/llama-3.3-nemotron-super-49b-v1.5\": {\"swebench\": 76.5, \"code_score\": 76.5, \"src\": \"estimate\"},\n \"qwen/qwen3.5-397b-a17b\": {\"swebench\": 76.4, \"code_score\": 76.4, \"src\": \"swebench\"},\n \"nvidia/llama-3.3-nemotron-super-49b-v1\": {\"swebench\": 76.4, \"livecodebench\": 83.6, \"code_score\": 76.4, \"src\": \"swebench\"},\n # ---- strong (60-75) ----\n \"moonshotai/kimi-k2-instruct-0905\": {\"swebench\": 73.0, \"code_score\": 73.0, \"src\": \"estimate\"},\n \"mistralai/devstral-2-123b-instruct-2512\": {\"swebench\": 72.2, \"code_score\": 72.2, \"src\": \"swebench\"},\n \"moonshotai/kimi-k2-thinking\": {\"swebench\": 71.0, \"code_score\": 71.0, \"src\": \"estimate\"},\n \"qwen/qwen3-coder-480b-a35b-instruct\": {\"swebench\": 70.0, \"code_score\": 70.0, \"src\": \"swebench\"},\n \"moonshotai/kimi-k2-instruct\": {\"swebench\": 70.0, \"code_score\": 70.0, \"src\": \"estimate\"},\n \"qwen/qwen3.5-122b-a10b\": {\"swebench\": 70.0, \"code_score\": 70.0, \"src\": \"estimate\"},\n \"z-ai/glm4.7\": {\"livecodebench\": 84.9, \"humaneval\": 94.2, \"code_score\": 70.0, \"src\": \"estimate\"},\n \"nvidia/llama-3.1-nemotron-ultra-253b-v1\": {\"swebench\": 68.0, \"code_score\": 68.0, \"src\": \"estimate\"},\n \"deepseek-ai/deepseek-v3.2\": {\"swebench\": 67.8, \"code_score\": 67.8, \"src\": \"swebench\"},\n \"deepseek-ai/deepseek-v3\": {\"swebench\": 67.8, \"code_score\": 67.8, \"src\": \"swebench\"},\n \"deepseek-ai/deepseek-v3.1-terminus\": {\"swebench\": 62.0, \"code_score\": 62.0, \"src\": \"estimate\"},\n \"nvidia/nemotron-3-super-120b-a12b\": {\"swebench\": 60.5, \"code_score\": 60.5, \"src\": \"swebench\"},\n # ---- mid (35-60) ----\n \"mistralai/mistral-large-3-675b-instruct-2512\":{\"swebench\": 58.0, \"code_score\": 58.0, \"src\": \"estimate\"},\n \"meta/llama-4-maverick-17b-128e-instruct\": {\"swebench\": 55.0, \"code_score\": 55.0, \"src\": \"estimate\"},\n \"qwen/qwen2.5-72b-instruct\": {\"livecodebench\": 55.5, \"humaneval\": 86.6, \"code_score\": 50.0, \"src\": \"livecodebench\"},\n \"qwen/qwen3-next-80b-a3b-thinking\": {\"swebench\": 50.0, \"code_score\": 50.0, \"src\": \"estimate\"},\n \"qwen/qwen2.5-coder-32b-instruct\": {\"livecodebench\": 38.0, \"humaneval\": 92.7, \"code_score\": 50.0, \"src\": \"estimate\"},\n \"deepseek-ai/deepseek-r1\": {\"swebench\": 49.2, \"livecodebench\": 65.9, \"code_score\": 49.2, \"src\": \"swebench\"},\n \"mistralai/mistral-medium-3.5-128b\": {\"swebench\": 45.0, \"code_score\": 45.0, \"src\": \"estimate\"},\n \"meta/llama-3.1-405b-instruct\": {\"livecodebench\": 85.0, \"humaneval\": 89.0, \"code_score\": 45.0, \"src\": \"estimate\"},\n \"mistralai/mistral-medium-3-instruct\": {\"swebench\": 40.0, \"code_score\": 40.0, \"src\": \"estimate\"},\n # ---- lower / fast tier (<= 35) ----\n \"meta/llama-3.3-70b-instruct\": {\"livecodebench\": 33.3, \"humaneval\": 88.4, \"code_score\": 30.0, \"src\": \"estimate\"},\n \"mistralai/mistral-large-2-instruct\": {\"swebench\": 30.0, \"humaneval\": 92.0, \"code_score\": 32.0, \"src\": \"estimate\"},\n \"qwen/qwen3-next-80b-a3b-instruct\": {\"swebench\": 30.0, \"code_score\": 30.0, \"src\": \"swebench\"},\n \"openai/gpt-oss-120b\": {\"swebench\": 30.0, \"livecodebench\": 60.0, \"code_score\": 30.0, \"src\": \"swebench\"},\n \"nvidia/llama-3.1-nemotron-70b-instruct\": {\"humaneval\": 80.0, \"code_score\": 30.0, \"src\": \"estimate\"},\n \"01-ai/yi-large\": {\"humaneval\": 81.8, \"code_score\": 28.0, \"src\": \"estimate\"},\n \"meta/llama-3.1-70b-instruct\": {\"livecodebench\": 28.5, \"humaneval\": 80.5, \"code_score\": 25.0, \"src\": \"estimate\"},\n \"google/gemma-3-27b-it\": {\"humaneval\": 80.0, \"code_score\": 25.0, \"src\": \"estimate\"},\n \"mistralai/mixtral-8x22b-instruct-v0.1\": {\"livecodebench\": 25.0, \"humaneval\": 75.0, \"code_score\": 22.0, \"src\": \"estimate\"},\n \"google/gemma-2-27b-it\": {\"humaneval\": 71.0, \"code_score\": 18.0, \"src\": \"estimate\"},\n \"openai/gpt-oss-20b\": {\"swebench\": 15.0, \"code_score\": 15.0, \"src\": \"estimate\"},\n \"meta/llama-3.1-8b-instruct\": {\"humaneval\": 72.6, \"code_score\": 12.0, \"src\": \"estimate\"},\n}\n\ndef code_score_for(model: str) -> Optional[dict]:\n \"\"\"Return {'score': float, 'src': str, ...} for a model, or None if unknown.\"\"\"\n b = BENCHMARKS.get(model)\n if not b:\n return None\n return {\n \"score\": b.get(\"code_score\"),\n \"src\": b.get(\"src\", \"?\"),\n \"swebench\": b.get(\"swebench\"),\n \"livecodebench\": b.get(\"livecodebench\"),\n \"humaneval\": b.get(\"humaneval\"),\n }\n\ndef combined_score(code: Optional[float], ttft_ms: Optional[int], tau_ms: float) -> Optional[float]:\n \"\"\"Combine quality + speed using TTFT only. Higher = better.\n\n combined = code_score * exp(-TTFT_ms / tau_ms)\n\n At TTFT=tau, you keep ~37% of the model's quality score. tau=3000ms means\n a 3s TTFT discounts a model heavily, while sub-300ms keeps ~90% of credit.\n\n This formula ignores tok/s (post-TTFT generation speed). For a metric\n that accounts for both, use smart_score().\n \"\"\"\n if code is None or ttft_ms is None:\n return None\n return code * math.exp(-ttft_ms / tau_ms)\n\n# Floor for measured tok/s in smart_score. Our probes use max_tokens=16, which\n# makes tok/s readings noisy — a real production model averaging 80 tok/s might\n# look like 2 tok/s on the probe because the first few chunks dominate.\n# Treating anything below this floor as \"the floor\" prevents probe noise from\n# tanking otherwise-good models. Real models on free NIM stream at 10+ tok/s\n# steady-state once warmed up.\n_TOK_PER_S_FLOOR = 10.0\n\ndef smart_score(\n code: Optional[float],\n ttft_ms: Optional[int],\n tok_per_s: Optional[float],\n tau_ms: float,\n output_tokens: int = 200,\n) -> Optional[float]:\n \"\"\"Combine quality + end-to-end response time. Higher = better.\n\n smart = code_score * exp(-effective_ms / tau_ms)\n where effective_ms = TTFT_ms + (output_tokens / max(tok_per_s, FLOOR)) * 1000\n\n Models the user's wait for a typical Claude Code response (default 200\n output tokens — medium response length). A model with great TTFT but\n abnormally slow throughput gets penalized just like one with poor TTFT\n — both leave the user staring at the screen.\n\n The tok/s floor (10/s) prevents noisy probe measurements from trashing\n a model. With max_tokens=16 probes we routinely see 1-3 tok/s for models\n that stream at 50-200 tok/s in production; the floor stops that from\n dominating the ranking.\n\n If tok_per_s is missing entirely, falls back to combined_score (TTFT-only).\n \"\"\"\n if code is None or ttft_ms is None:\n return None\n if tok_per_s is None or tok_per_s <= 0:\n return combined_score(code, ttft_ms, tau_ms)\n effective_tps = max(tok_per_s, _TOK_PER_S_FLOOR)\n effective_ms = ttft_ms + (output_tokens / effective_tps) * 1000.0\n # effective_ms is typically 10-30x larger than ttft_ms (most of the time\n # is spent generating tokens, not waiting for the first one). Scale tau\n # by 10 internally so the same --tau value produces sensible numbers for\n # both combined (TTFT-only) and smart (TTFT + output time). With tau=3000,\n # smart's effective tau is 30000ms — a 30s end-to-end response keeps 37%.\n smart_tau = tau_ms * 10\n return code * math.exp(-effective_ms / smart_tau)\n\n# ============================================================\n# === providers/base.py\n# ============================================================\n\n# Heuristics for non-chat endpoints we should never probe with /chat/completions.\nNON_CHAT_PATTERNS = re.compile(\n r\"(embed|rerank|guard|nemoguard|safety|nv-embed|colpali|\"\n r\"asr|speech|tts|riva|stable-diffusion|sdxl|flux|\"\n r\"vision-only|nv-clip|clip-)\",\n re.I,\n)\n\nclass Provider(Protocol):\n \"\"\"One inference backend. Implementations live in this package.\"\"\"\n\n name: str\n \"\"\"Short CLI-friendly identifier, e.g. 'nvidia-nim'.\"\"\"\n\n api_base: str\n \"\"\"Base URL, e.g. 'https://integrate.api.nvidia.com/v1'.\"\"\"\n\n proxy_value_prefix: str\n \"\"\"The string that goes before the model id when writing to .env.\n E.g. 'nvidia_nim/' -> a model 'meta/llama-3' becomes 'nvidia_nim/meta/llama-3'.\n Empty string for providers that use the model id verbatim.\"\"\"\n\n def __init__(self, api_key: str): ...\n\n def list_models(self, timeout: float = 15.0) -> list[str]:\n \"\"\"Return all model ids available to this key.\"\"\"\n ...\n\n def probe(\n self,\n model: str,\n timeout: float,\n rate_limiter: Optional[RateLimiter] = None,\n retry_on_429: bool = True,\n ) -> dict:\n \"\"\"Send one short streaming chat request, return TTFT + tokens.\n\n Returns a dict with one of:\n - on success: {model, ttft_ms, total_ms, tokens, tok_per_s, sample}\n - on failure: {model, error: str}\n \"\"\"\n ...\n\n# ============================================================\n# === providers/nvidia_nim.py\n# ============================================================\n\nPROMPT = \"Reply with exactly the single word: pong.\"\nMAX_TOKENS = 16\n\nclass NvidiaNimProvider:\n \"\"\"OpenAI-compatible client for NVIDIA NIM.\"\"\"\n\n name = \"nvidia-nim\"\n api_base = \"https://integrate.api.nvidia.com/v1\"\n proxy_value_prefix = \"nvidia_nim/\"\n\n def __init__(self, api_key: str):\n if not api_key:\n raise ValueError(\"NVIDIA API key is required\")\n self.api_key = api_key\n\n def list_models(self, timeout: float = 15.0) -> list[str]:\n req = urllib.request.Request(\n f\"{self.api_base}/models\",\n headers={\"Authorization\": f\"Bearer {self.api_key}\"},\n )\n with urllib.request.urlopen(req, timeout=timeout) as r:\n data = json.loads(r.read().decode(\"utf-8\"))\n ids = [m.get(\"id\") for m in data.get(\"data\", []) if m.get(\"id\")]\n return sorted(set(ids))\n\n def probe(\n self,\n model: str,\n timeout: float,\n rate_limiter: Optional[RateLimiter] = None,\n retry_on_429: bool = True,\n ) -> dict:\n body = {\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": PROMPT}],\n \"max_tokens\": MAX_TOKENS,\n \"temperature\": 0,\n \"stream\": True,\n }\n data = json.dumps(body).encode(\"utf-8\")\n req = urllib.request.Request(\n f\"{self.api_base}/chat/completions\",\n data=data,\n method=\"POST\",\n headers={\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"text/event-stream\",\n },\n )\n if rate_limiter is not None:\n rate_limiter.wait()\n t0 = time.perf_counter()\n ttft: Optional[float] = None\n tokens = 0\n text_parts: list[str] = []\n try:\n with urllib.request.urlopen(req, timeout=timeout) as r:\n for raw in r:\n if not raw:\n continue\n line = raw.decode(\"utf-8\", errors=\"replace\").strip()\n if not line.startswith(\"data:\"):\n continue\n payload = line[5:].strip()\n if payload == \"[DONE]\":\n continue\n try:\n j = json.loads(payload)\n except json.JSONDecodeError:\n continue\n choices = j.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n chunk = delta.get(\"content\")\n if chunk:\n if ttft is None:\n ttft = time.perf_counter() - t0\n tokens += 1\n text_parts.append(chunk)\n except urllib.error.HTTPError as e:\n if e.code == 429 and retry_on_429:\n ra = e.headers.get(\"Retry-After\") if e.headers else None\n try:\n wait = float(ra) if ra else 5.0\n except (TypeError, ValueError):\n wait = 5.0\n wait = min(max(wait, 1.0), 30.0)\n time.sleep(wait)\n return self.probe(model, timeout, rate_limiter, retry_on_429=False)\n body_txt = \"\"\n try:\n body_txt = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n except Exception:\n pass\n return {\"model\": model, \"error\": f\"HTTP {e.code} {body_txt}\"}\n except urllib.error.URLError as e:\n reason = getattr(e, \"reason\", e)\n if isinstance(reason, TimeoutError) or \"timed out\" in str(reason).lower():\n return {\"model\": model, \"error\": f\"timeout >{timeout:.1f}s\"}\n return {\"model\": model, \"error\": f\"URL {reason}\"}\n except TimeoutError:\n return {\"model\": model, \"error\": f\"timeout >{timeout:.1f}s\"}\n except Exception as e: # noqa: BLE001\n return {\"model\": model, \"error\": f\"{type(e).__name__}: {e}\"}\n total = time.perf_counter() - t0\n return {\n \"model\": model,\n \"ttft_ms\": int(ttft * 1000) if ttft is not None else None,\n \"total_ms\": int(total * 1000),\n \"tokens\": tokens,\n \"tok_per_s\": round(tokens / total, 1) if tokens and total > 0 else None,\n \"sample\": \"\".join(text_parts)[:60],\n }\n\n# ============================================================\n# === providers/openrouter.py\n# ============================================================\n\nPROMPT = \"Reply with exactly the single word: pong.\"\nMAX_TOKENS = 16\n\nclass OpenRouterProvider:\n \"\"\"OpenAI-compatible client for OpenRouter.\"\"\"\n\n name = \"openrouter\"\n api_base = \"https://openrouter.ai/api/v1\"\n proxy_value_prefix = \"openrouter/\"\n\n def __init__(self, api_key: str):\n if not api_key:\n raise ValueError(\"OpenRouter API key is required (sk-or-v1-...)\")\n self.api_key = api_key\n # OpenRouter recommends setting these to attribute usage. They're\n # optional; we keep the values minimal and override-able from env.\n self.referer = os.environ.get(\n \"OPENROUTER_HTTP_REFERER\",\n \"https://github.com/ChenghengLi/claude-free-installer\",\n )\n self.app_title = os.environ.get(\"OPENROUTER_X_TITLE\", \"claude-free-audit\")\n\n def _headers(self) -> dict[str, str]:\n return {\n \"Authorization\": f\"Bearer {self.api_key}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"text/event-stream\",\n \"HTTP-Referer\": self.referer,\n \"X-Title\": self.app_title,\n }\n\n def list_models(self, timeout: float = 15.0) -> list[str]:\n req = urllib.request.Request(\n f\"{self.api_base}/models\",\n headers={\"Authorization\": f\"Bearer {self.api_key}\"},\n )\n with urllib.request.urlopen(req, timeout=timeout) as r:\n data = json.loads(r.read().decode(\"utf-8\"))\n ids = [m.get(\"id\") for m in data.get(\"data\", []) if m.get(\"id\")]\n return sorted(set(ids))\n\n def probe(\n self,\n model: str,\n timeout: float,\n rate_limiter: Optional[RateLimiter] = None,\n retry_on_429: bool = True,\n ) -> dict:\n body = {\n \"model\": model,\n \"messages\": [{\"role\": \"user\", \"content\": PROMPT}],\n \"max_tokens\": MAX_TOKENS,\n \"temperature\": 0,\n \"stream\": True,\n }\n data = json.dumps(body).encode(\"utf-8\")\n headers = self._headers()\n # OpenRouter's chat endpoint is /chat/completions, same shape as OpenAI.\n req = urllib.request.Request(\n f\"{self.api_base}/chat/completions\",\n data=data,\n method=\"POST\",\n headers=headers,\n )\n if rate_limiter is not None:\n rate_limiter.wait()\n t0 = time.perf_counter()\n ttft: Optional[float] = None\n tokens = 0\n text_parts: list[str] = []\n try:\n with urllib.request.urlopen(req, timeout=timeout) as r:\n for raw in r:\n if not raw:\n continue\n line = raw.decode(\"utf-8\", errors=\"replace\").strip()\n # OpenRouter sometimes emits SSE comments (lines starting\n # with \":\") to keep the connection alive — ignore those.\n if not line.startswith(\"data:\"):\n continue\n payload = line[5:].strip()\n if payload == \"[DONE]\":\n continue\n try:\n j = json.loads(payload)\n except json.JSONDecodeError:\n continue\n choices = j.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n chunk = delta.get(\"content\")\n if chunk:\n if ttft is None:\n ttft = time.perf_counter() - t0\n tokens += 1\n text_parts.append(chunk)\n except urllib.error.HTTPError as e:\n if e.code == 429 and retry_on_429:\n ra = e.headers.get(\"Retry-After\") if e.headers else None\n try:\n wait = float(ra) if ra else 5.0\n except (TypeError, ValueError):\n wait = 5.0\n wait = min(max(wait, 1.0), 30.0)\n time.sleep(wait)\n return self.probe(model, timeout, rate_limiter, retry_on_429=False)\n body_txt = \"\"\n try:\n body_txt = e.read().decode(\"utf-8\", errors=\"replace\")[:200]\n except Exception:\n pass\n return {\"model\": model, \"error\": f\"HTTP {e.code} {body_txt}\"}\n except urllib.error.URLError as e:\n reason = getattr(e, \"reason\", e)\n if isinstance(reason, TimeoutError) or \"timed out\" in str(reason).lower():\n return {\"model\": model, \"error\": f\"timeout >{timeout:.1f}s\"}\n return {\"model\": model, \"error\": f\"URL {reason}\"}\n except TimeoutError:\n return {\"model\": model, \"error\": f\"timeout >{timeout:.1f}s\"}\n except Exception as e: # noqa: BLE001\n return {\"model\": model, \"error\": f\"{type(e).__name__}: {e}\"}\n total = time.perf_counter() - t0\n return {\n \"model\": model,\n \"ttft_ms\": int(ttft * 1000) if ttft is not None else None,\n \"total_ms\": int(total * 1000),\n \"tokens\": tokens,\n \"tok_per_s\": round(tokens / total, 1) if tokens and total > 0 else None,\n \"sample\": \"\".join(text_parts)[:60],\n }\n\n# ============================================================\n# === providers/__init__.py\n# ============================================================\n\n# Registry of available providers. Keys are CLI-friendly short names.\n_PROVIDERS: dict[str, type[Provider]] = {\n \"nvidia-nim\": NvidiaNimProvider,\n \"openrouter\": OpenRouterProvider,\n}\n\ndef register_providers() -> dict[str, type[Provider]]:\n \"\"\"Return the provider registry. Mutate to add more.\"\"\"\n return _PROVIDERS\n\ndef get_provider(name: str) -> type[Provider]:\n if name not in _PROVIDERS:\n raise KeyError(f\"Unknown provider: {name!r}. Known: {list(_PROVIDERS)}\")\n return _PROVIDERS[name]\n\n# ============================================================\n# === measure.py\n# ============================================================\n\ndef _median(values: list[int]) -> int:\n return int(statistics.median(values))\n\ndef measure(\n provider: Provider,\n model: str,\n runs: int,\n timeout: float,\n do_warmup: bool,\n rate_limiter: Optional[RateLimiter] = None,\n warmup_timeout: Optional[float] = None,\n) -> dict:\n \"\"\"Warmup + `runs` measured probes. Returns aggregated stats or {error: str}.\"\"\"\n if do_warmup:\n # Cold-start can be much slower than steady-state on NIM (some models\n # take 60-90s on first call). Give the warmup a bigger budget than\n # measured runs so we don't blackball slow-cold/fast-warm models.\n wt = warmup_timeout if warmup_timeout is not None else max(timeout * 2.5, 90.0)\n provider.probe(model, wt, rate_limiter=rate_limiter) # discard\n samples: list[dict] = []\n for _ in range(runs):\n samples.append(provider.probe(model, timeout, rate_limiter=rate_limiter))\n ok = [s for s in samples if not s.get(\"error\") and s.get(\"ttft_ms\") is not None]\n if not ok:\n first_err = next((s for s in samples if s.get(\"error\")), {\"error\": \"no successful runs\"})\n return {\"model\": model, \"error\": first_err.get(\"error\")}\n ttfts = [s[\"ttft_ms\"] for s in ok]\n totals = [s[\"total_ms\"] for s in ok]\n tpss = [s[\"tok_per_s\"] for s in ok if s.get(\"tok_per_s\")]\n return {\n \"model\": model,\n \"ttft_ms\": _median(ttfts),\n \"ttft_min_ms\": min(ttfts),\n \"ttft_max_ms\": max(ttfts),\n \"total_ms\": _median(totals),\n \"tok_per_s\": round(statistics.mean(tpss), 1) if tpss else None,\n \"samples\": len(ok),\n \"sample_text\": ok[-1].get(\"sample\", \"\"),\n }\n\n# ============================================================\n# === commands/__init__.py\n# ============================================================\n\n\n\n# ============================================================\n# === commands/audit.py\n# ============================================================\n\ndef select_candidates(args: argparse.Namespace, available: list[str]) -> list[str]:\n \"\"\"Build the candidate list from CLI flags + the available model set.\"\"\"\n chat_only = [m for m in available if not NON_CHAT_PATTERNS.search(m)]\n\n if args.early_exit:\n # Quality-priority walk: every benchmarked model present in the\n # account, sorted by code_score descending. We use BENCHMARKS as\n # the source of truth so calibrate sees every known model regardless\n # of CURATED ordering.\n if args.filter:\n f = args.filter.lower()\n base = [m for m in chat_only if f in m.lower() and m in BENCHMARKS]\n else:\n base = [m for m in BENCHMARKS if m in chat_only]\n cands = sorted(base, key=lambda m: -(BENCHMARKS[m].get(\"code_score\") or 0))\n elif args.all:\n cands = chat_only\n elif args.filter:\n f = args.filter.lower()\n cands = [m for m in chat_only if f in m.lower()]\n else:\n cands = [m for m in CURATED if m in chat_only]\n\n for extra in args.include:\n if extra in available and extra not in cands:\n cands.append(extra)\n\n seen: set[str] = set()\n out: list[str] = []\n for m in cands:\n if m in seen:\n continue\n seen.add(m)\n out.append(m)\n if len(out) >= args.max:\n break\n return out\n\ndef _maybe_write_env(\n args: argparse.Namespace,\n env_file: Path,\n proxy_value: str,\n C: dict[str, str],\n) -> None:\n if args.no_set:\n print(f\"{C['D']}--no-set: leaving {env_file} alone.{C['N']}\")\n return\n if not env_file.exists():\n print(\n f\"{C['Y']}Skipping .env update: {env_file} not found \"\n f\"(install claude-free first).{C['N']}\"\n )\n return\n if not args.auto_set:\n try:\n ans = input(\n f\"\\nSet {C['B']}MODEL_OPUS / MODEL_SONNET / MODEL_HAIKU / MODEL{C['N']} \"\n f\"to {C['G']}{proxy_value}{C['N']} in {env_file}? [Y/n] \"\n ).strip().lower()\n except EOFError:\n ans = \"\"\n if ans not in (\"\", \"y\", \"yes\"):\n print(\"Skipped -- .env unchanged.\")\n return\n for k in (\"MODEL_OPUS\", \"MODEL_SONNET\", \"MODEL_HAIKU\", \"MODEL\"):\n write_env_key(env_file, k, proxy_value)\n print(f\"{C['G']}Updated {env_file}.{C['N']}\")\n print(\n f\"{C['D']}Restart any running proxy: `claude-free stop && claude-free` \"\n f\"to pick up the new tier mapping.{C['N']}\"\n )\n\ndef _early_exit_path(\n args: argparse.Namespace,\n provider: Provider,\n candidates: list[str],\n limiter: Optional[RateLimiter],\n env_file: Path,\n C: dict[str, str],\n) -> int:\n \"\"\"Quality-priority walk: stop at first model whose TTFT <= threshold.\"\"\"\n threshold = args.threshold if args.threshold > 0 else 1000.0\n measured_timeout = (threshold / 1000.0) + 0.5\n warmup_timeout = max(measured_timeout * 5, 10.0)\n ee_runs = 1\n print(\n f\" walking {len(candidates)} benchmarked model(s) by code-score desc, \"\n f\"first one with TTFT <= {C['G']}{threshold:.0f} ms{C['N']} wins\\n\"\n f\" {C['D']}per-probe timeout: warmup {warmup_timeout:.0f}s, measured \"\n f\"{measured_timeout:.1f}s, {ee_runs} measured run/model{C['N']}\\n\"\n )\n winner_record: Optional[dict] = None\n all_results: list[dict] = []\n for i, m in enumerate(candidates, 1):\n bench = code_score_for(m) or {}\n cs = bench.get(\"score\")\n cs_str = f\"{cs:.1f}\" if cs is not None else \"n/a\"\n print(\n f\" [{i:>2}/{len(candidates)}] {m:<55} \"\n f\"code={C['G']}{cs_str:>5}{C['N']} \",\n end=\"\", flush=True,\n )\n r = measure(\n provider, m, ee_runs, measured_timeout,\n do_warmup=not args.no_warmup,\n rate_limiter=limiter,\n warmup_timeout=warmup_timeout,\n )\n r[\"code_score\"] = cs\n r[\"code_src\"] = bench.get(\"src\")\n all_results.append(r)\n if r.get(\"error\"):\n print(f\"{C['R']}{r['error'][:60]}{C['N']}\")\n continue\n r[\"combined\"] = combined_score(cs, r.get(\"ttft_ms\"), args.tau)\n r[\"smart\"] = smart_score(cs, r.get(\"ttft_ms\"), r.get(\"tok_per_s\"), args.tau, args.output_tokens)\n ttft = r[\"ttft_ms\"]\n ok_speed = ttft is not None and ttft <= threshold\n tag = f\"{C['G']}WINNER{C['N']}\" if ok_speed else f\"{C['Y']}slow{C['N']}\"\n print(\n f\"TTFT {C['B']}{ttft:>5} ms{C['N']} \"\n f\"({r['samples']} runs) -> {tag}\"\n )\n if ok_speed:\n winner_record = r\n break\n\n if winner_record is None:\n ok = [\n r for r in all_results\n if not r.get(\"error\")\n and r.get(\"ttft_ms\") is not None\n and r.get(\"combined\") is not None\n ]\n if not ok:\n print(f\"\\n{C['R']}No model met threshold and no successful probes either.{C['N']}\")\n return 1\n ok.sort(key=lambda r: -r[\"combined\"])\n winner_record = ok[0]\n print(\n f\"\\n{C['Y']}No model met TTFT <= {threshold:.0f} ms. \"\n f\"Falling back to best combined score: {winner_record['model']} \"\n f\"(TTFT {winner_record['ttft_ms']} ms, code {winner_record.get('code_score')}).{C['N']}\"\n )\n\n winner = winner_record[\"model\"]\n proxy_value = winner if winner.startswith(provider.proxy_value_prefix) else f\"{provider.proxy_value_prefix}{winner}\"\n print(\n f\"\\n{C['B']}Picked:{C['N']} {C['G']}{winner}{C['N']} \"\n f\"(TTFT {winner_record['ttft_ms']} ms, code {winner_record.get('code_score')}, \"\n f\"src {winner_record.get('code_src', '?')})\"\n )\n print(f\" -> {proxy_value}\")\n _maybe_write_env(args, env_file, proxy_value, C)\n return 0\n\ndef _full_sweep_path(\n args: argparse.Namespace,\n provider: Provider,\n candidates: list[str],\n limiter: Optional[RateLimiter],\n env_file: Path,\n C: dict[str, str],\n) -> int:\n \"\"\"Probe every candidate, print three rankings (TTFT / code / combined).\"\"\"\n runs = max(1, args.runs)\n print(\n f\" probing {len(candidates)} model(s), \"\n f\"{'no warmup, ' if args.no_warmup else '1 warmup + '}{runs} measured run(s) each, \"\n f\"timeout {args.timeout:.0f}s\\n\"\n )\n\n width = max(len(m) for m in candidates) + 2\n width = min(width, 55)\n header = (\n f\" {'MODEL':<{width}} {'TTFT(ms)':>10} {'TOK/S':>7} \"\n f\"{'CODE':>6} {'SMART':>7} SRC\"\n )\n print(f\"{C['B']}{header}{C['N']}\")\n print(\" \" + \"-\" * (width + 50))\n\n results: list[dict] = []\n for m in candidates:\n print(f\" {m:<{width}} \", end=\"\", flush=True)\n r = measure(\n provider, m, runs, args.timeout,\n do_warmup=not args.no_warmup,\n rate_limiter=limiter,\n )\n bench = code_score_for(m)\n if bench:\n r[\"code_score\"] = bench[\"score\"]\n r[\"code_src\"] = bench[\"src\"]\n r[\"bench_detail\"] = bench\n if r.get(\"error\"):\n print(f\"{C['R']}ERROR{C['N']} {r['error'][:80]}\")\n else:\n r[\"combined\"] = combined_score(r.get(\"code_score\"), r.get(\"ttft_ms\"), args.tau)\n r[\"smart\"] = smart_score(\n r.get(\"code_score\"), r.get(\"ttft_ms\"), r.get(\"tok_per_s\"),\n args.tau, args.output_tokens,\n )\n cs = r.get(\"code_score\")\n sm = r.get(\"smart\")\n cs_str = f\"{cs:.1f}\" if cs is not None else \"-\"\n sm_str = f\"{sm:.1f}\" if sm is not None else \"-\"\n tok = r.get(\"tok_per_s\")\n tok_str = f\"{tok}\" if tok is not None else \"-\"\n print(\n f\"{r['ttft_ms']:>10} \"\n f\"{tok_str:>7} \"\n f\"{cs_str:>6} \"\n f\"{sm_str:>7} \"\n f\"{r.get('code_src', '?')}\"\n )\n results.append(r)\n\n ok = [r for r in results if not r.get(\"error\") and r.get(\"ttft_ms\") is not None]\n if not ok:\n print(f\"\\n{C['R']}No successful probes -- nothing to rank.{C['N']}\")\n return 1\n\n def _print_ranking(title, sorted_list, score_fn, score_label):\n print(f\"\\n{C['B']}{title}:{C['N']}\")\n for i, r in enumerate(sorted_list, 1):\n marker = \"*\" if i == 1 else \" \"\n score = score_fn(r)\n score_str = f\"{score:.1f}\" if isinstance(score, float) else str(score)\n print(\n f\" {marker} {i:>2}. {r['model']:<{width}} \"\n f\"{C['G']}{score_str:>7}{C['N']} {score_label} \"\n f\"({C['D']}TTFT {r['ttft_ms']} ms, \"\n f\"code {r.get('code_score') if r.get('code_score') is not None else 'n/a'}{C['N']})\"\n )\n\n by_ttft = sorted(ok, key=lambda r: r[\"ttft_ms\"])\n by_code = sorted(\n [r for r in ok if r.get(\"code_score\") is not None],\n key=lambda r: -r[\"code_score\"],\n )\n by_combined = sorted(\n [r for r in ok if r.get(\"combined\") is not None],\n key=lambda r: -r[\"combined\"],\n )\n by_smart = sorted(\n [r for r in ok if r.get(\"smart\") is not None],\n key=lambda r: -r[\"smart\"],\n )\n\n _print_ranking(\"Lowest TTFT\", by_ttft, lambda r: r[\"ttft_ms\"], \"ms\")\n if by_code:\n _print_ranking(\"Best code-benchmark score\", by_code, lambda r: r[\"code_score\"], \"pts\")\n if by_smart:\n _print_ranking(\n f\"Smart score (TTFT + {args.output_tokens} output tokens, tau={args.tau:.0f}ms)\",\n by_smart,\n lambda r: r[\"smart\"],\n \"score\",\n )\n\n if args.by == \"smart\" and by_smart:\n winner_record = by_smart[0]\n ranking_used = \"smart (TTFT + output time)\"\n elif args.by == \"combined\" and by_combined:\n winner_record = by_combined[0]\n ranking_used = \"combined (TTFT only)\"\n elif args.by == \"code\" and by_code:\n winner_record = by_code[0]\n ranking_used = \"code benchmark\"\n else:\n winner_record = by_ttft[0]\n ranking_used = \"lowest TTFT\"\n if args.by != \"ttft\":\n print(\n f\"\\n{C['Y']}note: --by {args.by} had no eligible models \"\n f\"(missing benchmark / throughput data); falling back to TTFT.{C['N']}\"\n )\n\n winner = winner_record[\"model\"]\n proxy_value = winner if winner.startswith(provider.proxy_value_prefix) else f\"{provider.proxy_value_prefix}{winner}\"\n print(\n f\"\\n{C['B']}Winner ({ranking_used}):{C['N']} {C['G']}{winner}{C['N']} \"\n f\"-> {proxy_value}\"\n )\n _maybe_write_env(args, env_file, proxy_value, C)\n return 0\n\ndef run_audit(args: argparse.Namespace, provider: Provider) -> int:\n \"\"\"Entry point for `claude-free audit` and `claude-free calibrate`.\"\"\"\n C = colors()\n env_file = Path(args.env_file) if args.env_file else env_path()\n limiter = RateLimiter(args.rate) if args.rate and args.rate > 0 else None\n\n print(f\"{C['B']}claude-free audit{C['N']} {C['D']}-- ranking {provider.name} chat models by TTFT and code benchmarks{C['N']}\\n\")\n if limiter is not None:\n print(f\" {C['D']}rate limit: {args.rate:.0f} req/min sliding window (sleep before bursts){C['N']}\")\n\n print(f\" fetching model catalogue from {provider.api_base}/models ...\", end=\" \", flush=True)\n try:\n available = provider.list_models()\n except Exception as e: # noqa: BLE001\n print(f\"{C['R']}failed{C['N']}: {e}\")\n return 1\n print(f\"{C['G']}{len(available)} models{C['N']}\")\n\n candidates = select_candidates(args, available)\n if not candidates:\n print(f\"{C['Y']}No matching models. Try --all or --filter .{C['N']}\")\n return 1\n\n if args.early_exit:\n return _early_exit_path(args, provider, candidates, limiter, env_file, C)\n return _full_sweep_path(args, provider, candidates, limiter, env_file, C)\n\n# ============================================================\n# === commands/update.py\n# ============================================================\n\nURL = \"https://raw.githubusercontent.com/ChenghengLi/claude-free-installer/main/claude-free-audit.py\"\n\ndef run_update(_args, _provider) -> int:\n print(\n \"claude-free update -- in development mode, run one of these to refresh \"\n \"the deployed single-file artifact:\\n\\n\"\n f\" Linux/macOS: curl -fsSL {URL} -o ~/.local/bin/claude-free-audit.py\\n\"\n f\" Windows: iwr {URL} -OutFile $HOME\\\\.local\\\\bin\\\\claude-free-audit.py\\n\\n\"\n \"Or, in a development checkout, regenerate it locally:\\n\\n\"\n \" python tools/build.py\\n\"\n )\n return 0\n\n# ============================================================\n# === cli.py\n# ============================================================\n\ndef _build_audit_parser(p: argparse.ArgumentParser) -> None:\n \"\"\"Add the audit/calibrate flags. calibrate is `audit --set --early-exit`.\"\"\"\n p.add_argument(\"--all\", action=\"store_true\", help=\"probe every chat-capable model in your account\")\n p.add_argument(\"--filter\", default=\"\", help=\"only probe model ids containing this substring\")\n p.add_argument(\"--include\", action=\"append\", default=[], help=\"add a specific model id (repeatable)\")\n p.add_argument(\"--runs\", type=int, default=3, help=\"measured runs per model after 1 warmup (default 3)\")\n p.add_argument(\"--timeout\", type=float, default=45.0, help=\"per-probe timeout in seconds (default 45)\")\n p.add_argument(\"--max\", type=int, default=15, help=\"max number of models to probe (default 15)\")\n p.add_argument(\"--no-warmup\", action=\"store_true\", help=\"skip the warmup probe\")\n p.add_argument(\n \"--by\",\n choices=(\"smart\", \"combined\", \"ttft\", \"code\"),\n default=\"smart\",\n help=(\n \"rank winner by: smart (default, code * exp(-(TTFT + output/tok_per_s) / tau)), \"\n \"combined (TTFT-only variant), ttft (fastest), or code (best benchmarks)\"\n ),\n )\n p.add_argument(\n \"--tau\",\n type=float,\n default=3000.0,\n help=\"latency penalty constant in ms (default 3000). lower = penalize slow models harder\",\n )\n p.add_argument(\n \"--output-tokens\",\n type=int,\n default=200,\n help=\"typical response length used by smart_score (default 200 tokens)\",\n )\n p.add_argument(\n \"--early-exit\",\n action=\"store_true\",\n help=\"walk every benchmarked model in code-score-desc order; stop at first model with TTFT <= --threshold\",\n )\n p.add_argument(\n \"--threshold\",\n type=float,\n default=0.0,\n help=\"TTFT threshold in ms for --early-exit (default 1000 when --early-exit, else off)\",\n )\n p.add_argument(\n \"--rate\",\n type=float,\n default=30.0,\n help=\"max requests/min to the provider (default 30; free NIM ceiling is ~40). 0 = no limit\",\n )\n g = p.add_mutually_exclusive_group()\n g.add_argument(\"--set\", dest=\"auto_set\", action=\"store_true\", help=\"auto-set winner to all tiers (no prompt)\")\n g.add_argument(\"--no-set\", dest=\"no_set\", action=\"store_true\", help=\"never offer to set the winner\")\n p.add_argument(\"--key\", help=\"provider API key (overrides env / .env)\")\n p.add_argument(\n \"--provider\",\n default=\"nvidia-nim\",\n choices=list(register_providers().keys()),\n help=\"which inference provider to probe (default: nvidia-nim)\",\n )\n p.add_argument(\"--env-file\", help=\"path to free-claude-code .env (default ~/free-claude-code/.env)\")\n\ndef build_parser() -> argparse.ArgumentParser:\n p = argparse.ArgumentParser(\n prog=\"claude-free-audit\",\n description=\"Audit, calibrate, and switch NVIDIA NIM models for Claude Code.\",\n )\n p.add_argument(\"--version\", action=\"version\", version=f\"claude-free-audit {__version__}\")\n sub = p.add_subparsers(dest=\"command\")\n\n p_audit = sub.add_parser(\n \"audit\",\n help=\"probe models for TTFT + code benchmarks, print three rankings\",\n )\n _build_audit_parser(p_audit)\n\n p_calibrate = sub.add_parser(\n \"calibrate\",\n help=\"walk by code-score, pick first model with TTFT <= 1s, write to .env\",\n )\n _build_audit_parser(p_calibrate)\n\n sub.add_parser(\"update\", help=\"show how to refresh the deployed audit script\")\n\n # Back-compat: if no subcommand, accept all the audit flags directly\n # (so `claude-free-audit.py --early-exit ...` keeps working as before\n # without an explicit `audit` subcommand).\n _build_audit_parser(p)\n return p\n\ndef main(argv: list[str]) -> int:\n parser = build_parser()\n args = parser.parse_args(argv)\n\n if args.command == \"update\":\n return run_update(args, None)\n\n # calibrate is audit with extra defaults baked in\n if args.command == \"calibrate\":\n if not args.early_exit:\n args.early_exit = True\n if args.threshold == 0:\n args.threshold = 1000.0\n if not args.auto_set and not args.no_set:\n args.auto_set = True\n\n api_key = args.key or resolve_api_key(args.provider)\n if not api_key:\n env_var = {\n \"nvidia-nim\": \"NVIDIA_NIM_API_KEY\",\n \"openrouter\": \"OPENROUTER_API_KEY\",\n }.get(args.provider, args.provider.upper().replace(\"-\", \"_\") + \"_API_KEY\")\n print(\n f\"No API key for provider '{args.provider}'. \"\n f\"Set ${env_var} or populate ~/free-claude-code/.env, \"\n f\"or pass --key.\",\n file=sys.stderr,\n )\n return 2\n\n provider_cls = get_provider(args.provider)\n provider = provider_cls(api_key)\n return run_audit(args, provider)\n\n\n\nif __name__ == \"__main__\":\n import sys\n sys.exit(main(sys.argv[1:]))\n"}}}}