SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
4.5 KB · 109 lines python
Raw Blame History
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