Python 88.3%
TypeScript 7.6%
Shell 4.1%
1#!/usr/bin/env python32"""Re-runnable xAI model discovery.34GET /v1/models -> sanitized sources/xai/models-api-raw.json (mixed catalogue, 12 ids on 2026-09-19)5GET /v1/language-models -> sources/xai/language-models-raw.json6GET /v1/image-generation-models -> sources/xai/image-generation-models-raw.json7GET /v1/video-generation-models -> sources/xai/video-generation-models-raw.json8GET /v1/embedding-models -> sources/xai/embedding-models-raw.json9Diff of /v1/models against the previous file -> sources/xai/models-diff.json, prints NEW / REMOVED / CHANGED ids10(alias changes and price changes count as CHANGED; prices are ticks, 1 USD = 1e10).1112Usage: python3 scripts/discover_xai_models.py [--dry-run] [--base https://us.api.x.ai]13Auth comes from .env via scripts/live.py (never printed). Each call is logged to reports/live-requests.jsonl.14All these endpoints are free (no usage object is returned).15"""16from __future__ import annotations1718import argparse19import json20import os21import sys22from pathlib import Path2324sys.path.insert(0, str(Path(__file__).resolve().parent))25import live # noqa: E4022627ROOT = Path(__file__).resolve().parents[1]28SRC = ROOT / "sources" / "xai"29RAW = SRC / "models-api-raw.json"30DIFF = SRC / "models-diff.json"31TYPED = {32 "/v1/language-models": SRC / "language-models-raw.json",33 "/v1/image-generation-models": SRC / "image-generation-models-raw.json",34 "/v1/video-generation-models": SRC / "video-generation-models-raw.json",35 "/v1/embedding-models": SRC / "embedding-models-raw.json",36}373839def get(path: str, note: str) -> dict:40 st, body, _ = live.xai_request("GET", path, note=note)41 if st != 200:42 raise SystemExit(f"GET {path} -> HTTP {st}: {json.dumps(body)[:300]}")43 return body444546def index(doc: dict) -> dict[str, dict]:47 return {m["id"]: m for m in doc.get("data", [])}484950def fmt_price(m: dict) -> str:51 if m.get("prompt_text_token_price") is not None:52 return (f"${m['prompt_text_token_price'] / 1e4:.2f}/{m['cached_prompt_text_token_price'] / 1e4:.2f}/"53 f"{m['completion_text_token_price'] / 1e4:.2f} per 1M, ctx={m.get('context_length')}")54 if m.get("image_price") is not None:55 return f"${m['image_price'] / 1e10:.3f}/image"56 return "(no price fields)"575859def main() -> None:60 ap = argparse.ArgumentParser()61 ap.add_argument("--dry-run", action="store_true", help="fetch and diff, do not write")62 ap.add_argument("--base", default=None, help="override base URL (e.g. https://us.api.x.ai); typed lists + write skipped")63 args = ap.parse_args()64 if args.base:65 os.environ["XAI_BASE_URL"] = args.base66 live.XAI_BASE = args.base6768 prev = json.loads(RAW.read_text()) if RAW.exists() and not args.base else {"data": []}69 cur = get("/v1/models", f"discover models{' @' + args.base if args.base else ''}")70 cur["_retrieved_at"] = live.now()71 pi, ci = index(prev), index(cur)72 new = sorted(set(ci) - set(pi))73 removed = sorted(set(pi) - set(ci))74 changed = sorted(i for i in set(ci) & set(pi) if ci[i] != pi[i])75 print(f"models: {len(ci)} (previous {len(pi)}) base={args.base or live.XAI_BASE}")76 for i in new:77 print(f"NEW {i} {fmt_price(ci[i])} aliases={ci[i].get('aliases')}")78 for i in removed:79 print(f"REMOVED {i}")80 for i in changed:81 keys = sorted(k for k in set(ci[i]) | set(pi[i]) if ci[i].get(k) != pi[i].get(k))82 print(f"CHANGED {i} fields={keys}")83 if not (new or removed or changed):84 print("no changes")85 if args.base:86 for m in cur["data"]:87 print(f" {m['id']}: {fmt_price(m)}")88 return8990 typed: dict[str, dict] = {}91 for path, dest in TYPED.items():92 typed[path] = get(path, "discover typed model list")93 print(f"{path}: {len(typed[path].get('models', []))} models")9495 diff = {"retrieved_at": cur["_retrieved_at"], "previous_count": len(pi), "current_count": len(ci), "new": new, "removed": removed,96 "changed": [{"id": i, "before": pi[i], "after": ci[i]} for i in changed],97 "typed_counts": {p: len(d.get("models", [])) for p, d in typed.items()}}98 if args.dry_run:99 return100 live.save_sanitized({k: v for k, v in cur.items() if not k.startswith("_")}, RAW)101 for path, dest in TYPED.items():102 live.save_sanitized(typed[path], dest)103 live.save_sanitized(diff, DIFF)104 print(f"wrote {RAW.relative_to(ROOT)}, typed raws and {DIFF.relative_to(ROOT)}")105106107if __name__ == "__main__":108 main()109