SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
13.5 KB · 244 lines python
Raw Blame History
1"""OpenRouter — public model catalogue `https://openrouter.ai/api/v1/models` (one JSON document, ≈ 440 entries). Tier 2: official2provider pages win conflicts.34Each entry: `id` = `vendor/slug[:variant]`, `name` ("Vendor: Model"), `description`, `context_length`, `architecture` (input/output5modalities, tokenizer), `pricing` (USD per token as strings → ×1e6 = USD per 1M tokens), `top_provider.max_completion_tokens`,6`supported_parameters`, `created` (epoch), `hugging_face_id`. Entries whose id starts with `~` are alias rows (`alias_target`) and are7skipped; `-1` prices (dynamic routing) are skipped.89Model identity: `{"openrouter": "<vendor/slug>"}` (variant suffix stripped) + `{"hf_repo": …}` when the catalogue gives it; aliases and the10developer organisation come from `connectors/_identity.py` (never a vendor `*_model_id` — OpenRouter slugs differ from the labs' API ids).11`created` is the OpenRouter *listing* date (`openrouter_listed_at`), never a release date.12Pricing: every price is booked on the **OpenRouter provider entity only** (`provider_ref("openrouter")`) with `features.upstream_provider` =13the registry provider key of the routed vendor when it is one — never on the lab's own provider entity, which would create a second14"current" price per model × provider. `provider_model_id` = the full OpenRouter id (variants `:free` / `:thinking` / `:nitro` are separate15price rows of the same model, `features.variant`).16"""17from __future__ import annotations1819import re20from datetime import UTC, datetime21from typing import Any2223from aiatlas.connectors._identity import family_ref, model_identity, org_key_for_vendor, org_ref_in24from aiatlas.ontology.taxonomy import normalize_modalities25from aiatlas.registry import provider_by_openrouter, provider_ref, providers26from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext27from aiatlas.sdk.facts import EntityRef, Facts, Target28from aiatlas.sdk.fetch import FetchResult2930URL = "https://openrouter.ai/api/v1/models"31VENDOR_PROVIDER = {"x-ai": "xai", "mistralai": "mistral"}32VARIANTS = {"free", "extended", "nitro", "floor", "online", "thinking", "beta", "exacto", "fast"}333435class OpenRouterConnector(BaseConnector):36    name = "openrouter"37    label = "OpenRouter — model catalogue with routed pricing"38    description = "Public JSON catalogue: context, modalities, supported parameters and per-token prices of every model routed by OpenRouter."39    source_key = "openrouter.ai"40    version = "2"41    parser_version = "2"42    interval_seconds = 3 * 360043    min_interval_seconds = 360044    max_interval_seconds = 8640045    rate_per_min = 1046    tier = 247    priority = 148    expected_min_records = 15049    concurrency = 15051    async def discover(self, ctx: RunContext) -> list[Target]:52        return [Target(url=URL, doc_type="catalogue", key="models", min_bytes=50000, meta={"content_type": "application/json"}, priority=1)]5354    async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:55        facts = Facts()56        data = parsed.json if parsed.kind == "json" else None57        items = data.get("data") if isinstance(data, dict) else None58        if not isinstance(items, list):59            return facts60        aggregator = provider_ref("openrouter")61        facts.entities.append(aggregator)62        for item in items:63            try:64                self._model(facts, item, aggregator)65            except Exception as exc:  # noqa: BLE001 — one malformed row must not sink the catalogue66                ctx.log.warning("openrouter row skipped", extra={"id": item.get("id") if isinstance(item, dict) else None, "error": str(exc)})67        facts.document_entity = aggregator68        facts.document_title = "OpenRouter models"69        return facts7071    def _model(self, facts: Facts, item: dict[str, Any], aggregator: EntityRef) -> None:72        full_id = item.get("id") or ""73        if not full_id or full_id.startswith("~") or item.get("alias_target"):74            return75        vendor, _, rest = full_id.partition("/")76        if not rest:77            return78        base_slug, _, variant = rest.partition(":")79        variant = variant.lower() if variant else None80        base_id = f"{vendor}/{base_slug}"81        name = _display_name(item.get("name") or base_slug, vendor)82        if vendor == "openrouter":83            self._router_product(facts, item, aggregator, full_id, base_id, name, variant)84            return85        org, provider_key = self._vendor(facts, vendor, item.get("name") or "", base_slug)86        ids = {"openrouter": base_id}87        hf = item.get("hugging_face_id")88        if isinstance(hf, str) and re.fullmatch(r"[\w.-]+/[\w.-]+", hf):89            ids["hf_repo"] = hf90        ref = next((e for e in facts.entities if e.entity_type == "model" and e.identifiers.get("openrouter") == base_id), None)91        if ref is None:92            ident = model_identity(base_id)          # untrusted: aliases only (OpenRouter slugs ≠ vendor API ids)93            aliases = [a for a in dict.fromkeys([item.get("name"), base_id, base_slug, *(ident.aliases if ident else [])]) if a and a != name]94            ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=aliases, family=family_ref(name, org), identity_confidence="medium")95            if org:96                facts.relate(org, "develops", ref)97            facts.claim(ref, "openrouter_id", base_id)98            if "hf_repo" in ids:99                facts.claim(ref, "hf_repo", hf)100            if not variant:101                self._claims(facts, ref, item)102        elif not variant:103            self._claims(facts, ref, item)104        price = self._price(facts, ref, item, provider_key, vendor, aggregator, full_id, variant)105        if price is None and not variant:106            # still note the listing so `available_through` exists even without a usable price107            facts.claim(ref, "openrouter_listed", True)108109    def _router_product(self, facts: Facts, item: dict[str, Any], aggregator: EntityRef, full_id: str, base_id: str, name: str, variant: str | None) -> None:110        """`openrouter/auto`, `openrouter/pareto-code`, `openrouter/free`… are OpenRouter's own routing products, not models: a `product`111        entity (kind router) operated by OpenRouter. Their price (when not dynamic `-1`) is booked against the product."""112        ref = next((e for e in facts.entities if e.entity_type == "product" and e.identifiers.get("openrouter") == base_id), None)113        if ref is None:114            org = org_ref_in(facts, "openrouter")115            ref = facts.entity("product", name, identifiers={"openrouter": base_id}, organization=org, aliases=[a for a in {item.get("name"), base_id} if a and a != name],116                               attributes={"kind": "router"}, identity_confidence="high")117            facts.claim(ref, "openrouter_id", base_id)118            facts.claim(ref, "description", (item.get("description") or "").strip()[:2000] or None)119            facts.claim(ref, "context_length", _int(item.get("context_length")), unit="tokens")120            facts.relate(aggregator, "operates", ref)121        self._price(facts, ref, item, "openrouter", "openrouter", aggregator, full_id, variant)122123    def _claims(self, facts: Facts, ref: EntityRef, item: dict[str, Any]) -> None:124        arch = item.get("architecture") or {}125        top = item.get("top_provider") or {}126        facts.claim(ref, "description", (item.get("description") or "").strip()[:2000] or None)127        facts.claim(ref, "context_length", _int(item.get("context_length")), unit="tokens")128        facts.claim(ref, "max_output_tokens", _int(top.get("max_completion_tokens")), unit="tokens")129        raw_in = [m for m in arch.get("input_modalities") or [] if isinstance(m, str)]130        raw_out = [m for m in arch.get("output_modalities") or [] if isinstance(m, str)]131        mi, mo = normalize_modalities(raw_in), normalize_modalities(raw_out)      # "file" → document (ontology)132        if "file" in raw_in:133            facts.claim(ref, "file_input", True)134        facts.claim(ref, "modalities_input", mi)135        facts.claim(ref, "modalities_output", mo)136        facts.claim(ref, "modalities", sorted(set(mi) | set(mo)))137        tok = arch.get("tokenizer")138        facts.claim(ref, "tokenizer", tok if isinstance(tok, str) and tok.lower() not in ("router", "other") else None)139        facts.claim(ref, "instruct_type", arch.get("instruct_type"))140        params = sorted({p for p in item.get("supported_parameters") or [] if isinstance(p, str)})141        facts.claim(ref, "supported_parameters", params)142        if params:143            facts.claim(ref, "tool_calling", "tools" in params)144            facts.claim(ref, "structured_output", "structured_outputs" in params or "response_format" in params)145        reasoning = item.get("reasoning")146        if isinstance(reasoning, dict) or "reasoning" in params:147            facts.claim(ref, "reasoning", True)148        if "image" in mi:149            facts.claim(ref, "vision", True)150        created = item.get("created")151        if isinstance(created, (int, float)) and created > 1_000_000_000:152            # the catalogue's `created` is when OpenRouter listed the model — a listing date, never the model's release date153            facts.claim(ref, "openrouter_listed_at", datetime.fromtimestamp(created, tz=UTC).date().isoformat())154        facts.claim(ref, "knowledge_cutoff", _month(item.get("knowledge_cutoff")))155        facts.claim(ref, "openrouter_expiration_date", item.get("expiration_date") if isinstance(item.get("expiration_date"), str) else None)156157    def _price(self, facts: Facts, ref: EntityRef, item: dict[str, Any], provider_key: str | None, vendor: str, aggregator: EntityRef, full_id: str,158               variant: str | None) -> Any:159        pricing = item.get("pricing") or {}160        prompt, completion = _per_mtok(pricing.get("prompt")), _per_mtok(pricing.get("completion"))161        if prompt is None and completion is None:162            return None163        if (prompt is not None and prompt < 0) or (completion is not None and completion < 0):164            return None165        top = item.get("top_provider") or {}166        features: dict[str, Any] = {"via": "openrouter", "upstream_vendor": vendor}167        if provider_key:168            features["upstream_provider"] = provider_key           # registry provider key of the routed lab — the row still belongs to OpenRouter169        if variant:170            features["variant"] = variant171        for key in ("web_search", "internal_reasoning", "audio", "audio_output", "image_output", "input_cache_write_1h"):172            v = pricing.get(key)173            if isinstance(v, str) and v not in ("0", "-1", ""):174                features[key] = v175        if pricing.get("overrides"):176            features["tiered_pricing"] = pricing["overrides"]177        per_image = _float(pricing.get("image"))178        per_request = _float(pricing.get("request"))179        return facts.price(model=ref, provider=aggregator, provider_model_id=full_id, input_per_mtok=prompt, output_per_mtok=completion,180                           cached_input_per_mtok=_per_mtok(pricing.get("input_cache_read")), cache_write_per_mtok=_per_mtok(pricing.get("input_cache_write")),181                           per_image=per_image if per_image else None, per_request=per_request if per_request else None,182                           context_length=_int(top.get("context_length") or item.get("context_length")), max_output_tokens=_int(top.get("max_completion_tokens")),183                           features=features, source_url=f"https://openrouter.ai/{full_id}", meta={"is_moderated": top.get("is_moderated")})184185    def _vendor(self, facts: Facts, vendor: str, name: str, base_slug: str) -> tuple[EntityRef | None, str | None]:186        provider_key = provider_by_openrouter(vendor) or VENDOR_PROVIDER.get(vendor)187        if provider_key and provider_key not in providers():188            provider_key = None189        org_key = org_key_for_vendor(vendor)190        if not org_key and provider_key:191            org_key = providers()[provider_key].get("organization")192        if not org_key:193            ident = model_identity(base_slug)194            org_key = ident.org_key if ident else None195        org = org_ref_in(facts, org_key)196        if org is None:197            display = name.split(":", 1)[0].strip() if ":" in name else vendor198            org = next((e for e in facts.entities if e.identifiers.get("openrouter_vendor") == vendor), None)199            if org is None:200                org = EntityRef(entity_type="company", name=display or vendor, identifiers={"openrouter_vendor": vendor}, aliases=[vendor], identity_confidence="medium")201                facts.entities.append(org)202        return org, provider_key203204205def _display_name(name: str, vendor: str) -> str:206    if ":" in name:207        prefix, rest = name.split(":", 1)208        if rest.strip() and (prefix.strip().lower().replace(" ", "-") in (vendor, vendor.replace("-", "")) or len(prefix) <= 24):209            return rest.strip()210    return name.strip()211212213def _per_mtok(v: Any) -> float | None:214    f = _float(v)215    if f is None:216        return None217    if f < 0:218        return -1.0219    return round(f * 1_000_000, 6)220221222def _float(v: Any) -> float | None:223    try:224        return float(v) if v not in (None, "") else None225    except (TypeError, ValueError):226        return None227228229def _int(v: Any) -> int | None:230    try:231        return int(v) if v is not None else None232    except (TypeError, ValueError):233        return None234235236def _month(v: Any) -> str | None:237    if not isinstance(v, str):238        return None239    m = re.match(r"(\d{4})-(\d{2})", v)240    return f"{m.group(1)}-{m.group(2)}" if m else None241242243CONNECTORS = [OpenRouterConnector]244