Python 88.3%
TypeScript 7.6%
Shell 4.1%
1#!/usr/bin/env python32"""Re-runnable Gemini model discovery (Gemini Developer API).34GET /v1beta/models (paginated: pageSize + pageToken) -> sanitized sources/gemini/models-api-raw.json5GET /v1/models (stable surface) -> sources/gemini/models-v1-raw.json6Diff against the previous v1beta file -> sources/gemini/models-diff.json, prints NEW / REMOVED / CHANGED7(limits, supportedGenerationMethods, thinking, version, defaults) and the v1-vs-v1beta id difference.89Usage: python3 scripts/discover_gemini_models.py [--page-size N] [--dry-run] [--skip-v1]10Auth comes from .env via scripts/live.py (x-goog-api-key header, never in the URL, never printed).11Each call is logged to reports/live-requests.jsonl (path is normalised to /v1beta/models).12"""13from __future__ import annotations1415import argparse16import json17import sys18from pathlib import Path1920sys.path.insert(0, str(Path(__file__).resolve().parent))21import live # noqa: E402 (scripts/live.py)2223ROOT = Path(__file__).resolve().parents[1]24RAW = ROOT / "sources" / "gemini" / "models-api-raw.json"25RAW_V1 = ROOT / "sources" / "gemini" / "models-v1-raw.json"26DIFF = ROOT / "sources" / "gemini" / "models-diff.json"2728# Fields whose change is worth flagging as CHANGED (order irrelevant for lists).29WATCH = ("inputTokenLimit", "outputTokenLimit", "supportedGenerationMethods", "thinking", "version",30 "temperature", "topP", "topK", "maxTemperature", "displayName", "description")313233def fetch_all(version: str, page_size: int) -> dict:34 models: list[dict] = []35 token: str | None = None36 pages = 037 while True:38 q = f"?pageSize={page_size}" + (f"&pageToken={token}" if token else "")39 st, body, _ = live.gemini_request("GET", f"/{version}/models{q}", note=f"discover models {version} page {pages + 1}")40 if st != 200:41 raise SystemExit(f"GET /{version}/models -> HTTP {st}: {json.dumps(body)[:300]}")42 models.extend(body.get("models", []))43 pages += 144 token = body.get("nextPageToken")45 if not token:46 break47 return {"models": models, "_pages": pages, "_retrieved_at": live.now(), "_version": version}484950def index(doc: dict) -> dict[str, dict]:51 return {m["name"].removeprefix("models/"): m for m in doc.get("models", [])}525354def changed_fields(a: dict, b: dict) -> list[str]:55 out = []56 for k in WATCH:57 va, vb = a.get(k), b.get(k)58 if isinstance(va, list) and isinstance(vb, list):59 if sorted(va) != sorted(vb):60 out.append(k)61 elif va != vb:62 out.append(k)63 return out646566def main() -> None:67 ap = argparse.ArgumentParser()68 ap.add_argument("--page-size", type=int, default=50, help="pageSize query param (docs: default 50, max 1000)")69 ap.add_argument("--dry-run", action="store_true", help="fetch and diff, do not write")70 ap.add_argument("--skip-v1", action="store_true", help="do not call GET /v1/models")71 args = ap.parse_args()7273 prev = json.loads(RAW.read_text()) if RAW.exists() else {"models": []}74 cur = fetch_all("v1beta", args.page_size)75 pi, ci = index(prev), index(cur)76 new = sorted(set(ci) - set(pi))77 removed = sorted(set(pi) - set(ci))78 changed = [{"id": i, "fields": changed_fields(pi[i], ci[i]), "before": {k: pi[i].get(k) for k in WATCH}, "after": {k: ci[i].get(k) for k in WATCH}}79 for i in sorted(set(ci) & set(pi)) if changed_fields(pi[i], ci[i])]8081 v1_ids: list[str] = []82 only_v1beta: list[str] = []83 only_v1: list[str] = []84 cur_v1 = None85 if not args.skip_v1:86 cur_v1 = fetch_all("v1", args.page_size)87 vi = index(cur_v1)88 v1_ids = sorted(vi)89 only_v1beta = sorted(set(ci) - set(vi))90 only_v1 = sorted(set(vi) - set(ci))9192 diff = {"retrieved_at": cur["_retrieved_at"], "previous_count": len(pi), "current_count": len(ci),93 "pages_fetched_v1beta": cur["_pages"], "page_size": args.page_size,94 "new": new, "removed": removed, "changed": changed,95 "v1": {"count": len(v1_ids), "only_in_v1beta": only_v1beta, "only_in_v1": only_v1} if cur_v1 else None}9697 print(f"v1beta models: {len(ci)} (previous {len(pi)}), pages={cur['_pages']} (pageSize={args.page_size})")98 for i in new:99 print(f"NEW {i} in={ci[i].get('inputTokenLimit')} out={ci[i].get('outputTokenLimit')} methods={ci[i].get('supportedGenerationMethods')}")100 for i in removed:101 print(f"REMOVED {i}")102 for c in changed:103 print(f"CHANGED {c['id']} fields={c['fields']}")104 if not (new or removed or changed):105 print("no changes vs previous v1beta snapshot")106 if cur_v1:107 print(f"v1 models: {len(v1_ids)}; only in v1beta: {only_v1beta or 'none'}; only in v1: {only_v1 or 'none'}")108 if args.dry_run:109 return110 live.save_sanitized({"models": cur["models"]}, RAW)111 if cur_v1:112 live.save_sanitized({"models": cur_v1["models"]}, RAW_V1)113 live.save_sanitized(diff, DIFF)114 print(f"wrote {RAW.relative_to(ROOT)}" + (f", {RAW_V1.relative_to(ROOT)}" if cur_v1 else "") + f" and {DIFF.relative_to(ROOT)}")115116117if __name__ == "__main__":118 main()119