#!/usr/bin/env python3 """Re-runnable Anthropic model discovery. GET /v1/models (paginated, limit=100) -> sanitized sources/anthropic/models-api-raw.json Diff against the previous file -> sources/anthropic/models-diff.json, prints NEW / REMOVED / CHANGED ids. Usage: python3 scripts/discover_anthropic_models.py [--limit N] [--dry-run] Auth comes from .env via scripts/live.py (never printed). Each call is logged to reports/live-requests.jsonl. """ from __future__ import annotations import argparse import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import live # noqa: E402 (scripts/live.py) ROOT = Path(__file__).resolve().parents[1] RAW = ROOT / "sources" / "anthropic" / "models-api-raw.json" DIFF = ROOT / "sources" / "anthropic" / "models-diff.json" def fetch_all(limit: int) -> dict: data: list[dict] = [] after: str | None = None pages = 0 while True: q = f"?limit={limit}" + (f"&after_id={after}" if after else "") st, body, _ = live.anthropic_request("GET", f"/v1/models{q}", note=f"discover models page {pages + 1}") if st != 200: raise SystemExit(f"GET /v1/models -> HTTP {st}: {json.dumps(body)[:300]}") data.extend(body.get("data", [])) pages += 1 if not body.get("has_more"): break after = body.get("last_id") if not after: break return {"data": data, "has_more": False, "first_id": data[0]["id"] if data else None, "last_id": data[-1]["id"] if data else None, "_pages": pages, "_retrieved_at": live.now()} def index(doc: dict) -> dict[str, dict]: return {m["id"]: m for m in doc.get("data", [])} def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--limit", type=int, default=100) ap.add_argument("--dry-run", action="store_true", help="fetch and diff, do not write") args = ap.parse_args() prev = json.loads(RAW.read_text()) if RAW.exists() else {"data": []} cur = fetch_all(args.limit) pi, ci = index(prev), index(cur) new = sorted(set(ci) - set(pi)) removed = sorted(set(pi) - set(ci)) changed = sorted(i for i in set(ci) & set(pi) if ci[i] != pi[i]) diff = {"retrieved_at": cur["_retrieved_at"], "previous_count": len(pi), "current_count": len(ci), "new": new, "removed": removed, "changed": [{"id": i, "before": pi[i], "after": ci[i]} for i in changed], "pages_fetched": cur["_pages"]} print(f"models: {len(ci)} (previous {len(pi)}), pages={cur['_pages']}") for i in new: print(f"NEW {i} created_at={ci[i].get('created_at')}") for i in removed: print(f"REMOVED {i}") for i in changed: print(f"CHANGED {i}") if not (new or removed or changed): print("no changes") if args.dry_run: return out = {k: v for k, v in cur.items() if not k.startswith("_")} live.save_sanitized(out, RAW) live.save_sanitized(diff, DIFF) print(f"wrote {RAW.relative_to(ROOT)} and {DIFF.relative_to(ROOT)}") if __name__ == "__main__": main()