94 lines
2.7 KiB
Python
94 lines
2.7 KiB
Python
"""Re-derive `claude_code_api.models` constants from a `claude` binary.
|
|
|
|
The `claude` CLI is a Mach-O (or ELF) executable that bundles its JS
|
|
runtime as plain strings. Model ids and the `/model` alias list show up
|
|
verbatim — no decryption needed. This script `strings`-greps a given
|
|
binary and prints a refreshed `src/claude_code_api/models.py`.
|
|
|
|
Usage:
|
|
uv run python scripts/extract_models.py # auto-locate
|
|
uv run python scripts/extract_models.py /path/to/claude # explicit
|
|
|
|
Auto-location order (macOS):
|
|
~/.local/share/claude/versions/<latest>
|
|
$(which claude) (follows symlinks)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_MODEL_ID_RE = re.compile(
|
|
rb'"(claude-(?:opus|sonnet|haiku|[0-9])[a-z0-9-]*-(?:opus|sonnet|haiku|[0-9])[a-z0-9-]*)"'
|
|
)
|
|
_ALIAS_LIST_RE = re.compile(rb'\["sonnet","opus","haiku".*?"opusplan"\]')
|
|
|
|
|
|
def find_default_binary() -> Path | None:
|
|
home = Path.home()
|
|
versions_dir = home / ".local" / "share" / "claude" / "versions"
|
|
if versions_dir.is_dir():
|
|
versions = sorted(
|
|
(p for p in versions_dir.iterdir() if p.is_file()),
|
|
key=lambda p: p.name,
|
|
)
|
|
if versions:
|
|
return versions[-1]
|
|
on_path = shutil.which("claude")
|
|
if on_path is not None:
|
|
return Path(on_path).resolve()
|
|
return None
|
|
|
|
|
|
def extract_strings(binary: Path) -> bytes:
|
|
result = subprocess.run(
|
|
["strings", str(binary)],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return result.stdout
|
|
|
|
|
|
def extract_model_ids(blob: bytes) -> set[str]:
|
|
return {m.group(1).decode("ascii") for m in _MODEL_ID_RE.finditer(blob)}
|
|
|
|
|
|
def extract_aliases(blob: bytes) -> list[str]:
|
|
match = _ALIAS_LIST_RE.search(blob)
|
|
if match is None:
|
|
return []
|
|
raw = match.group(0).decode("ascii")
|
|
return re.findall(r'"([^"]+)"', raw)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("binary", nargs="?", help="Path to claude executable")
|
|
args = parser.parse_args()
|
|
|
|
binary = Path(args.binary) if args.binary else find_default_binary()
|
|
if binary is None or not binary.is_file():
|
|
print(f"could not locate claude binary: {binary}", file=sys.stderr)
|
|
return 2
|
|
|
|
blob = extract_strings(binary)
|
|
ids = sorted(extract_model_ids(blob))
|
|
aliases = extract_aliases(blob)
|
|
|
|
print(f"# binary: {binary}", file=sys.stderr)
|
|
print(f"# {len(ids)} model ids, {len(aliases)} aliases", file=sys.stderr)
|
|
print("aliases:", aliases)
|
|
print("models:")
|
|
for model_id in ids:
|
|
print(f" {model_id}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|