File size: 9,118 Bytes
f707f5d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """Re-pull `inference-provider-capabilities.jsonl` from the HF Inference Providers catalog.
The snapshot next to this script is dated data (see the `.md`): the repo convention is to
re-pull whenever it is more than ~7 days old, because provider capability booleans, prices,
and even whole `(model, provider)` routes churn week to week.
What it does: fetch the catalog's backing endpoint (the same one the HF Inference Providers
page reads), keep only `status: "live"` provider rows, flatten each to one record, stamp every
record with the pull date, and write the JSONL sorted by model id (case-insensitively) then
provider β the stable order the file has always used, so a re-pull diffs cleanly.
It writes the DATA only. The `.md`'s prose β snapshot date, row/model counts, the provider
capability table, the notable-gaps bullets, the cheapest-routes table β is hand-maintained;
`--summary` prints the recomputed numbers to paste in, and `--diff` reports what changed
against the committed snapshot so the drift is easy to write up.
python refresh-capabilities.py --diff --summary # pull + report (writes the JSONL)
python refresh-capabilities.py --dry-run --diff # report only, write nothing
python refresh-capabilities.py --retrieved 2026-07-27 # override the stamp (default: today)
Needs no API key β the catalog endpoint is public.
"""
from __future__ import annotations
import argparse
import json
import urllib.request
from collections import defaultdict
from datetime import date
from pathlib import Path
from typing import Any
_HERE = Path(__file__).resolve().parent
OUT = _HERE / "inference-provider-capabilities.jsonl"
ENDPOINT = "https://router.huggingface.co/v1/models"
SOURCE = "https://huggingface.co/inference/models"
# Record field order β frozen so a re-pull produces a minimal diff. Documented in the .md.
FIELDS = ("model", "owned_by", "provider", "context_length", "input_price_per_mtok",
"output_price_per_mtok", "supports_tools", "supports_structured_output",
"is_model_author", "retrieved", "source", "endpoint")
def _price(v: Any) -> float | None:
"""USD per 1M tokens as the catalog reports it, rounded to kill float noise
(the endpoint returns e.g. 0.9299999999999999 for a $0.93 route)."""
return None if v is None else round(float(v), 6)
def fetch(endpoint: str = ENDPOINT) -> list[dict[str, Any]]:
with urllib.request.urlopen(endpoint, timeout=120) as resp:
return json.load(resp)["data"]
def flatten(models: list[dict[str, Any]], retrieved: str) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for m in models:
for p in m.get("providers") or []:
if p.get("status") != "live":
continue
pricing = p.get("pricing") or {}
rows.append({
"model": m["id"],
"owned_by": m.get("owned_by"),
"provider": p.get("provider"),
"context_length": p.get("context_length"),
"input_price_per_mtok": _price(pricing.get("input")),
"output_price_per_mtok": _price(pricing.get("output")),
# Absent/None reads as "the provider doesn't expose it" β coerce so the
# column is a clean boolean for consumers that filter on it.
"supports_tools": bool(p.get("supports_tools")),
"supports_structured_output": bool(p.get("supports_structured_output")),
"is_model_author": bool(p.get("is_model_author")),
"retrieved": retrieved,
"source": SOURCE,
"endpoint": endpoint_of(m),
})
rows.sort(key=lambda r: (r["model"].lower(), r["provider"]))
return rows
def endpoint_of(_model: dict[str, Any]) -> str:
return ENDPOINT
def _read_committed() -> list[dict[str, Any]]:
if not OUT.exists():
return []
return [json.loads(line) for line in OUT.read_text(encoding="utf-8").splitlines() if line.strip()]
def report_diff(old: list[dict[str, Any]], new: list[dict[str, Any]]) -> None:
"""What changed vs the committed snapshot, keyed by (model, provider) and ignoring
the `retrieved` stamp (which changes on every pull by construction)."""
def index(rows: list[dict[str, Any]]) -> dict[tuple[str, str], dict[str, Any]]:
return {(r["model"], r["provider"]): {k: v for k, v in r.items() if k != "retrieved"} for r in rows}
o, n = index(old), index(new)
prev_date = old[0].get("retrieved") if old else "(none)"
added, removed = sorted(n.keys() - o.keys()), sorted(o.keys() - n.keys())
flipped = [(k, {f: (o[k][f], n[k][f]) for f in o[k] if o[k][f] != n[k][f]})
for k in sorted(o.keys() & n.keys()) if o[k] != n[k]]
print(f"\n--- drift vs committed snapshot ({prev_date}) ---")
print(f"{len(added)} added, {len(removed)} removed, {len(flipped)} changed "
f"({len(o)} -> {len(n)} rows)")
for k in added:
print(f" + {k[0]} @ {k[1]}")
for k in removed:
print(f" - {k[0]} @ {k[1]}")
for k, d in flipped:
print(f" ~ {k[0]} @ {k[1]}: " + ", ".join(f"{f} {a}->{b}" for f, (a, b) in d.items()))
def report_summary(rows: list[dict[str, Any]]) -> None:
"""Recompute the hand-maintained numbers in the .md so they can be pasted in."""
both = [r for r in rows if r["supports_tools"] and r["supports_structured_output"]]
tools_only = [r for r in rows if r["supports_tools"] and not r["supports_structured_output"]]
print(f"\n--- .md summary numbers ---")
print(f"Rows: {len(rows)} live (model, provider) pairs across "
f"{len({r['model'] for r in rows})} models and {len({r['provider'] for r in rows})} providers.")
print(f"Both tools + structured output: {len(both)} provider rows across "
f"{len({r['model'] for r in both})} models. Tools-only: {len(tools_only)} rows.")
profile: dict[str, list[int]] = defaultdict(lambda: [0, 0, 0])
for r in rows:
e = profile[r["provider"]]
e[0] += 1
e[1] += bool(r["supports_tools"])
e[2] += bool(r["supports_tools"] and r["supports_structured_output"])
print("\n| Provider | live rows | tools | tools **+** structured output |")
print("| --- | ---: | ---: | ---: |")
# Same ordering as the .md: both-capable providers first (most structured-output
# routes), then the tools-only ones (most tool routes), then the capability-less
# bulk host β so the table reads as a capability ranking, not a volume ranking.
both_capable = sorted((p for p, e in profile.items() if e[2]), key=lambda p: -profile[p][2])
rest = sorted((p for p, e in profile.items() if not e[2] and e[1]), key=lambda p: -profile[p][1])
none = sorted((p for p, e in profile.items() if not e[1]), key=lambda p: -profile[p][0])
for p in [*both_capable, *rest, *none]:
live, t, b = profile[p]
print(f"| {p} | {live} | {t} | {b if b else '**0**'} |")
cheapest: dict[str, dict[str, Any]] = {}
for r in both:
if r["input_price_per_mtok"] is None:
continue
cur = cheapest.get(r["model"])
if cur is None or r["input_price_per_mtok"] < cur["input_price_per_mtok"]:
cheapest[r["model"]] = r
print("\n| Model | Cheapest both-capable provider | Context | $ In / Out (1M) |")
print("| --- | --- | ---: | --- |")
for r in sorted(cheapest.values(), key=lambda r: r["input_price_per_mtok"])[:5]:
ctx = f"{round((r['context_length'] or 0) / 1000)}K" if r["context_length"] else "?"
print(f"| {r['model']} | {r['provider']} | {ctx} | "
f"{r['input_price_per_mtok']} / {r['output_price_per_mtok']} |")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--retrieved", default=date.today().isoformat(),
help="snapshot date to stamp on every record (default: today)")
ap.add_argument("--dry-run", action="store_true", help="don't write the JSONL")
ap.add_argument("--diff", action="store_true", help="report drift vs the committed snapshot")
ap.add_argument("--summary", action="store_true", help="print the .md's hand-maintained numbers")
args = ap.parse_args()
old = _read_committed()
rows = flatten(fetch(), args.retrieved)
if args.diff:
report_diff(old, rows)
if args.summary:
report_summary(rows)
if args.dry_run:
print(f"\n(dry run β {len(rows)} rows not written)")
return 0
OUT.write_text("".join(json.dumps({k: r[k] for k in FIELDS}, separators=(",", ":"),
ensure_ascii=False) + "\n" for r in rows), encoding="utf-8")
print(f"\nWrote {len(rows)} rows to {OUT} (retrieved {args.retrieved}).")
print("Now update inference-provider-capabilities.md by hand: snapshot date, row/model "
"counts, provider table, notable-gaps bullets, cheapest-routes table.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|