"""Refresh the vendored models.dev snapshot used by the model catalog. Fetches https://models.dev/api.json (MIT-licensed, community-maintained), trims it to the providers OpenSquilla registers, and writes the compact snapshot consumed by ``opensquilla.provider.models_dev``. Usage:: uv run python scripts/refresh_models_dev_snapshot.py Review the diff before committing — the snapshot is deliberately small and human-reviewable so upstream data mistakes are caught at refresh time, not at runtime. ``check_snapshot_integrity`` refuses to write a snapshot that shrank suspiciously or silently lost a runtime provider's table. """ from __future__ import annotations import json import sys from collections.abc import Iterable from datetime import date from pathlib import Path import httpx from opensquilla.provider.registry import list_provider_specs API_URL = "https://models.dev/api.json" SNAPSHOT_PATH = ( Path(__file__).resolve().parents[1] / "src" / "opensquilla" / "provider" / "models_dev_snapshot.json" ) # OpenSquilla provider id -> models.dev provider ids (merged in order; the # first source of a model id wins). Derived from each registered spec's # ``catalog_source`` so this script cannot drift from the provider registry. # Script-only extras (sources for providers the registry does not carry) # would be added explicitly after the comprehension — none exist today. PROVIDER_SOURCES: dict[str, tuple[str, ...]] = { spec.provider_id: spec.catalog_source for spec in list_provider_specs() if spec.catalog_source } # models.dev ``cost`` field -> compact snapshot key. Both sides are USD per # MILLION tokens, so values are vendored verbatim (no unit conversion). _COST_KEYS: tuple[tuple[str, str], ...] = ( ("input", "in_mtok"), ("output", "out_mtok"), ("cache_read", "cr_mtok"), ("cache_write", "cw_mtok"), ) # A refresh that loses more than this fraction of the committed snapshot's # models is treated as an upstream incident (payload truncation, source-id # rename, …), not a routine cleanup: refuse to write. MAX_SHRINK_RATIO = 0.8 def _trim_model(entry: dict) -> dict | None: limit = entry.get("limit") or {} context = int(limit.get("context") or 0) output = int(limit.get("output") or 0) if context <= 0 and output <= 0: return None # Self-contradictory upstream data (context smaller than max output — # e.g. models.dev's openrouter z-ai/glm-5.1 entry) would poison budget # resolution; drop it so lookups fall through to a consistent layer. if 0 < context < output: return None modalities = entry.get("modalities") or {} inputs = {str(item).lower() for item in modalities.get("input") or []} trimmed = { "ctx": context, "out": output, "reasoning": bool(entry.get("reasoning")), "tools": bool(entry.get("tool_call")), "vision": "image" in inputs, } cost = entry.get("cost") if isinstance(cost, dict): for source_key, snapshot_key in _COST_KEYS: value = cost.get(source_key) # Vendor only flat per-Mtok leaf numbers. Some models.dev entries # nest tiered pricing (lists/dicts keyed by context bands) here; # tiers are deliberately ignored — a single misleading average is # worse than "unknown", and nuanced pricing is corrections-owned. if isinstance(value, (int, float)) and not isinstance(value, bool) and value >= 0: trimmed[snapshot_key] = value return trimmed def build_snapshot_providers( data: dict, provider_sources: dict[str, tuple[str, ...]] | None = None, ) -> dict[str, dict[str, dict]]: """Trim a raw models.dev ``api.json`` payload to the snapshot tables. Pure transform (no network, no filesystem) so tests can drive it with synthetic payloads. ``provider_sources`` defaults to the registry-derived ``PROVIDER_SOURCES`` mapping. """ sources_map = PROVIDER_SOURCES if provider_sources is None else provider_sources providers: dict[str, dict[str, dict]] = {} for osq_id, sources in sources_map.items(): table: dict[str, dict] = {} for source in sources: models = (data.get(source) or {}).get("models") or {} for model_id, entry in models.items(): key = str(model_id).strip().lower() if key in table: continue trimmed = _trim_model(entry) if trimmed is not None: table[key] = trimmed if table: providers[osq_id] = dict(sorted(table.items())) return providers def _provider_tables(snapshot: dict) -> dict[str, dict[str, dict]]: providers = snapshot.get("providers") return providers if isinstance(providers, dict) else {} def check_snapshot_integrity( new: dict, old: dict, required_provider_ids: Iterable[str] | None = None, ) -> list[str]: """Return human-readable reasons a freshly built snapshot must NOT be written. Pure comparison of the new snapshot dict against the committed one (both in on-disk shape, i.e. carrying a ``providers`` table) — no network, so the guards are unit-testable against synthetic dicts. An empty list means the snapshot is safe to write. Guards: - max-shrink: total model count below ``MAX_SHRINK_RATIO`` of the committed count means upstream truncation or source-id drift. - table regression: a runtime-supported provider with a non-empty ``catalog_source`` that HAS a committed table but produced zero entries lost its data — never silently degrade it to the synthesized floor. ``required_provider_ids`` defaults to every runtime-supported registry spec that declares a ``catalog_source``. """ errors: list[str] = [] new_tables = _provider_tables(new) old_tables = _provider_tables(old) new_total = sum(len(models) for models in new_tables.values()) old_total = sum(len(models) for models in old_tables.values()) if old_total > 0 and new_total < old_total * MAX_SHRINK_RATIO: errors.append( f"model count shrank from {old_total} to {new_total} " f"(< {MAX_SHRINK_RATIO:.0%} of the committed snapshot)" ) if required_provider_ids is None: required_provider_ids = [ spec.provider_id for spec in list_provider_specs() if spec.runtime_supported and spec.catalog_source ] for provider_id in required_provider_ids: if old_tables.get(provider_id) and not new_tables.get(provider_id): errors.append( f"provider {provider_id!r} produced zero entries but the " "committed snapshot has a table for it" ) return errors def main() -> int: data = httpx.get(API_URL, timeout=30.0, follow_redirects=True).json() providers = build_snapshot_providers(data) snapshot = { "_source": API_URL, "_license": "MIT (models.dev, maintained by the SST team)", "_fetched": date.today().isoformat(), "providers": providers, } try: committed = json.loads(SNAPSHOT_PATH.read_text(encoding="utf-8")) except (OSError, ValueError): committed = {} errors = check_snapshot_integrity(snapshot, committed) if errors: for error in errors: print(f"refusing to write snapshot: {error}", file=sys.stderr) return 1 SNAPSHOT_PATH.write_text(json.dumps(snapshot, indent=1, sort_keys=False) + "\n") total = sum(len(models) for models in providers.values()) print(f"wrote {SNAPSHOT_PATH} ({len(providers)} providers, {total} models)") return 0 if __name__ == "__main__": sys.exit(main())