HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
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