"""Phase 0 — inventory of OpenAI and Anthropic model IDs actually reachable with the project's keys. Stdlib only (urllib) so it runs before the venv exists. Keys are read from .env and never printed. One `GET /v1/models` per provider, then ONE tiny completion per selected model (max 8 output tokens). Writes logs/model_inventory.json and reports/model_inventory.md. Costs are computed from the token usage returned by the provider and the public list prices hard-coded below (checked 2026-11-01; see claude-api skill). """ from __future__ import annotations import json, os, time, urllib.request, urllib.error, sys, pathlib ROOT = pathlib.Path(__file__).resolve().parents[2] env = {} for line in (ROOT / ".env").read_text().splitlines(): if "=" in line and not line.startswith("#"): k, v = line.split("=", 1); env[k.strip()] = v.strip() OPENAI = env.get("OPENAI_API_KEY", ""); ANTHROPIC = env.get("ANTHROPIC_API_KEY", "") # USD per 1M tokens (input, output) — public list prices as of 2026-11-01 (verify before relying on them) PRICES = { "claude-fable-5-1": (25.0, 125.0), "claude-opus-5-5": (5.0, 25.0), "claude-sonnet-5-5": (3.0, 15.0), "claude-haiku-5-5": (1.0, 5.0), } def call(url, headers, body=None, timeout=60): req = urllib.request.Request(url, data=json.dumps(body).encode() if body else None, headers=headers, method="POST" if body else "GET") t0 = time.time() try: with urllib.request.urlopen(req, timeout=timeout) as r: return r.status, json.loads(r.read().decode()), round(time.time() - t0, 2) except urllib.error.HTTPError as e: return e.code, {"error": e.read().decode()[:300]}, round(time.time() - t0, 2) except Exception as e: # noqa: BLE001 return -1, {"error": str(e)[:300]}, round(time.time() - t0, 2) out = {"checked_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "openai": {}, "anthropic": {}} # ---- OpenAI -------------------------------------------------------------- if OPENAI: st, body, lat = call("https://api.openai.com/v1/models", {"Authorization": f"Bearer {OPENAI}"}) ids = sorted(m["id"] for m in body.get("data", [])) if st == 200 else [] out["openai"]["list_status"] = st; out["openai"]["n_models"] = len(ids); out["openai"]["model_ids"] = ids tests = {} for mid in [m for m in ("gpt-5.5", "gpt-5.1", "gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-4.1", "gpt-4.1-mini", "gpt-4o-mini") if m in ids][:4]: st, b, lat = call("https://api.openai.com/v1/chat/completions", {"Authorization": f"Bearer {OPENAI}", "Content-Type": "application/json"}, {"model": mid, "messages": [{"role": "user", "content": "Reply with the single word OK."}], "max_completion_tokens": 8}) u = b.get("usage", {}) tests[mid] = {"status": st, "latency_s": lat, "usage": u, "reply": (b.get("choices") or [{}])[0].get("message", {}).get("content") if st == 200 else b.get("error")} out["openai"]["tests"] = tests else: out["openai"]["list_status"] = "no key" # ---- Anthropic ----------------------------------------------------------- if ANTHROPIC: H = {"x-api-key": ANTHROPIC, "anthropic-version": "2023-06-01", "Content-Type": "application/json"} st, body, lat = call("https://api.anthropic.com/v1/models?limit=100", H) ids = sorted(m["id"] for m in body.get("data", [])) if st == 200 else [] out["anthropic"]["list_status"] = st; out["anthropic"]["n_models"] = len(ids); out["anthropic"]["model_ids"] = ids if st != 200: out["anthropic"]["list_error"] = body tests = {} for mid in [m for m in ("claude-fable-5-1", "claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5") if m in ids]: st, b, lat = call("https://api.anthropic.com/v1/messages", H, {"model": mid, "max_tokens": 8, "messages": [{"role": "user", "content": "Reply with the single word OK."}]}) u = b.get("usage", {}) cost = None if mid in PRICES and u: cost = round(u.get("input_tokens", 0) * PRICES[mid][0] / 1e6 + u.get("output_tokens", 0) * PRICES[mid][1] / 1e6, 6) tests[mid] = {"status": st, "latency_s": lat, "usage": u, "approx_cost_usd": cost, "reply": (b.get("content") or [{}])[0].get("text") if st == 200 else b.get("error")} out["anthropic"]["tests"] = tests else: out["anthropic"]["list_status"] = "no key" (ROOT / "logs").mkdir(exist_ok=True) (ROOT / "logs" / "model_inventory.json").write_text(json.dumps(out, indent=1, ensure_ascii=False)) lines = [f"# Model inventory (Phase 0) — checked {out['checked_utc']}", "", "Tiny test requests only (max 8 output tokens). Keys read from `.env`, never printed.", ""] for prov in ("openai", "anthropic"): d = out[prov]; lines.append(f"## {prov}"); lines.append(f"- `GET /v1/models` status: {d.get('list_status')} — {d.get('n_models', 0)} model IDs listed") if d.get("model_ids"): lines.append("- IDs: " + ", ".join(f"`{m}`" for m in d["model_ids"])) if d.get("tests"): lines += ["", "| model | status | latency (s) | usage | approx. cost (USD) | reply |", "|---|---|---|---|---|---|"] for m, t in d["tests"].items(): lines.append(f"| `{m}` | {t['status']} | {t['latency_s']} | {json.dumps(t['usage'])} | {t.get('approx_cost_usd', '—')} | {str(t['reply'])[:60]} |") lines.append("") (ROOT / "reports" / "model_inventory.md").write_text("\n".join(lines)) print("\n".join(lines))