OpenRouter routers are products, not models; official-org quantisations are artifacts with a same-org canonical (None when unsure)
`openrouter/*` ids (auto, auto-beta, pareto-code, free, fusion…) → `product` entities (kind router) operated by OpenRouter, price booked against the product when not dynamic. Hugging Face: a quant/precision token in the repo name makes the repo an artifact even under the developer's own org (nvidia/Gemma-4-31B-IT-NVFP4, tencent/HY-MT1.5-1.8B-FP8, LiquidAI/LFM2.5-230M-GGUF, black-forest-labs/FLUX.2-klein-4b-fp8); canonical = the model built from the stripped name under the family's developer (medium confidence), or None when nothing can be stripped or the developer is unknown — never an invented model. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4 changed files +62 −3
modified
src/aiatlas/connectors/hub/huggingface.py
+9 −3
@@ -393,13 +393,17 @@ class HuggingFaceConnector(BaseConnector): | |||||
| 393 | facts.claim(ref, "hf_repo", base_id) | 393 | facts.claim(ref, "hf_repo", base_id) | ||
| 394 | return ref | 394 | return ref | ||
| 395 | 395 | ||||
| 396 | − def _canonical_ref(self, facts: Facts, repo_id: str, analysis: NameAnalysis, bases: list[tuple[str, str | None]]) -> EntityRef: | 396 | + def _canonical_ref(self, facts: Facts, repo_id: str, analysis: NameAnalysis, bases: list[tuple[str, str | None]]) -> EntityRef | None: | ||
| 397 | − """The model an artifact packages: the `base_model` repo when present, otherwise the analysed base name (medium confidence).""" | 397 | + """The model an artifact packages: the `base_model` repo when present, otherwise the analysed base name (medium confidence) — | ||
| 398 | + `nvidia/Gemma-4-31B-IT-NVFP4` → same-org model "Gemma-4-31B-IT". None when the base cannot be named confidently (no token to strip | ||||
| 399 | + from the name, or a redistributor repo whose family is unknown): never an invented model.""" | ||||
| 398 | base = next((b for b, k in bases if k == "quantized" and "/" in b), None) or next((b for b, _ in bases if "/" in b), None) | 400 | base = next((b for b, k in bases if k == "quantized" and "/" in b), None) or next((b for b, _ in bases if "/" in b), None) | ||
| 399 | if base and base.lower() != repo_id.lower(): | 401 | if base and base.lower() != repo_id.lower(): | ||
| 400 | return self._base_ref(facts, base) | 402 | return self._base_ref(facts, base) | ||
| 401 | org_slug, _, repo_name = repo_id.partition("/") | 403 | org_slug, _, repo_name = repo_id.partition("/") | ||
| 402 | name = canonical_name(repo_name) | 404 | name = canonical_name(repo_name) | ||
| 405 | + if not name or name.lower() == repo_name.lower() or not re.search(r"[a-z]", name, re.IGNORECASE): | ||||
| 406 | + return None | ||||
| 403 | org: EntityRef | None = None | 407 | org: EntityRef | None = None | ||
| 404 | family_orgs = _family_orgs(repo_name) | 408 | family_orgs = _family_orgs(repo_name) | ||
| 405 | if family_orgs and org_slug.lower() in family_orgs: | 409 | if family_orgs and org_slug.lower() in family_orgs: | ||
@@ -408,7 +412,9 @@ class HuggingFaceConnector(BaseConnector): | |||||
| 408 | hf_org = next((o for o in sorted(family_orgs) if org_by_hf(o)), None) | 412 | hf_org = next((o for o in sorted(family_orgs) if org_by_hf(o)), None) | ||
| 409 | org = self._org_ref(facts, org_by_hf(hf_org)["hf_org"], None) if hf_org else None # type: ignore[index] | 413 | org = self._org_ref(facts, org_by_hf(hf_org)["hf_org"], None) if hf_org else None # type: ignore[index] | ||
| 410 | elif org_slug.lower() not in REDISTRIBUTORS: | 414 | elif org_slug.lower() not in REDISTRIBUTORS: | ||
| 411 | − org = self._org_ref(facts, org_slug, None) | 415 | + org = self._org_ref(facts, org_slug, None) # the official developer's own quantisation / conversion → its own model | ||
| 416 | + if org is None: | ||||
| 417 | + return None | ||||
| 412 | for e in facts.entities: | 418 | for e in facts.entities: | ||
| 413 | if e.entity_type == "model" and e.name.lower() == name.lower() and (org is None or e.organization is None or e.organization.identifiers == org.identifiers): | 419 | if e.entity_type == "model" and e.name.lower() == name.lower() and (org is None or e.organization is None or e.organization.identifiers == org.identifiers): | ||
| 414 | return e | 420 | return e | ||
modified
src/aiatlas/connectors/providers/openrouter.py
+17 −0
@@ -79,6 +79,9 @@ class OpenRouterConnector(BaseConnector): | |||||
| 79 | variant = variant.lower() if variant else None | 79 | variant = variant.lower() if variant else None | ||
| 80 | base_id = f"{vendor}/{base_slug}" | 80 | base_id = f"{vendor}/{base_slug}" | ||
| 81 | name = _display_name(item.get("name") or base_slug, vendor) | 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 | + return | ||||
| 82 | org, provider_key = self._vendor(facts, vendor, item.get("name") or "", base_slug) | 85 | org, provider_key = self._vendor(facts, vendor, item.get("name") or "", base_slug) | ||
| 83 | ids = {"openrouter": base_id} | 86 | ids = {"openrouter": base_id} | ||
| 84 | hf = item.get("hugging_face_id") | 87 | hf = item.get("hugging_face_id") | ||
@@ -103,6 +106,20 @@ class OpenRouterConnector(BaseConnector): | |||||
| 103 | # still note the listing so `available_through` exists even without a usable price | 106 | # still note the listing so `available_through` exists even without a usable price | ||
| 104 | facts.claim(ref, "openrouter_listed", True) | 107 | facts.claim(ref, "openrouter_listed", True) | ||
| 105 | 108 | ||||
| 109 | + 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) | ||||
| 122 | + | ||||
| 106 | def _claims(self, facts: Facts, ref: EntityRef, item: dict[str, Any]) -> None: | 123 | def _claims(self, facts: Facts, ref: EntityRef, item: dict[str, Any]) -> None: | ||
| 107 | arch = item.get("architecture") or {} | 124 | arch = item.get("architecture") or {} | ||
| 108 | top = item.get("top_provider") or {} | 125 | top = item.get("top_provider") or {} | ||
modified
tests/test_huggingface.py
+26 −0
@@ -114,6 +114,32 @@ def test_identity_helpers(): | |||||
| 114 | assert pipeline_modalities("text-to-image") == (["text"], ["image"]) and pipeline_modalities("unknown-tag") == ([], []) | 114 | assert pipeline_modalities("text-to-image") == (["text"], ["image"]) and pipeline_modalities("unknown-tag") == ([], []) | ||
| 115 | 115 | ||||
| 116 | 116 | ||||
| 117 | +def test_official_org_quantisations_are_artifacts(connector): | ||||
| 118 | + """A quant/precision token makes the repo an artifact even under the developer's own org; canonical = the same-org model from the stripped | ||||
| 119 | + name; None when the base cannot be named confidently.""" | ||||
| 120 | + from aiatlas.sdk.facts import Facts | ||||
| 121 | + | ||||
| 122 | + cases = {"nvidia/Gemma-4-31B-IT-NVFP4": ("quantization", "Gemma-4-31B-IT", "Google"), # Gemma is Google's family: NVIDIA quantises it | ||||
| 123 | + "tencent/HY-MT1.5-1.8B-FP8": ("quantization", "HY-MT1.5-1.8B", "Tencent"), | ||||
| 124 | + "LiquidAI/LFM2.5-230M-GGUF": ("quantization", "LFM2.5-230M", "Liquid AI"), "black-forest-labs/FLUX.2-klein-4b-fp8": ("quantization", "FLUX.2-klein-4b", "Black Forest Labs"), | ||||
| 125 | + "Qwen/Qwen3-8B-MLX-bf16": ("conversion", "Qwen3-8B", "Qwen")} | ||||
| 126 | + for repo, (kind, base, org_name) in cases.items(): | ||||
| 127 | + facts = Facts() | ||||
| 128 | + org = connector._org_ref(facts, repo.split("/")[0], None) | ||||
| 129 | + ref = connector._model_ref(facts, repo, org) | ||||
| 130 | + assert ref.entity_type == "artifact" and ref.artifact_kind == kind and ref.name == repo, repo | ||||
| 131 | + assert ref.canonical is not None and ref.canonical.entity_type == "model" and ref.canonical.name == base, repo | ||||
| 132 | + assert ref.canonical.organization.name == org_name and ref.canonical.identity_confidence == "medium", repo | ||||
| 133 | + assert not any(r.predicate == "develops" and r.object is ref for r in facts.relations) | ||||
| 134 | + # unsure → no invented model | ||||
| 135 | + facts = Facts() | ||||
| 136 | + ref = connector._model_ref(facts, "mradermacher/FooBar-GGUF", connector._org_ref(facts, "mradermacher", None)) | ||||
| 137 | + assert ref.entity_type == "artifact" and ref.canonical is None # redistributor, unknown family | ||||
| 138 | + facts = Facts() | ||||
| 139 | + ref = connector._model_ref(facts, "Qwen/Qwen3-8B", connector._org_ref(facts, "Qwen", None), quant_format="gguf") | ||||
| 140 | + assert ref.entity_type == "artifact" and ref.canonical is None # quantised per tags only: nothing to strip from the name | ||||
| 141 | + | ||||
| 142 | + | ||||
| 117 | async def test_daily_papers(connector): | 143 | async def test_daily_papers(connector): | ||
| 118 | facts = await extract_from_fixture(connector, Target(url="https://huggingface.co/papers", doc_type="listing", key="papers"), fixture_path("huggingface", "papers.html")) | 144 | facts = await extract_from_fixture(connector, Target(url="https://huggingface.co/papers", doc_type="listing", key="papers"), fixture_path("huggingface", "papers.html")) | ||
| 119 | papers = [e for e in facts.entities if e.entity_type == "paper"] | 145 | papers = [e for e in facts.entities if e.entity_type == "paper"] | ||
modified
tests/test_openrouter.py
+10 −0
@@ -35,6 +35,16 @@ async def test_catalogue(): | |||||
| 35 | assert not any(p.input_per_mtok is not None and p.input_per_mtok < 0 for p in facts.prices) # dynamic "-1" prices skipped | 35 | assert not any(p.input_per_mtok is not None and p.input_per_mtok < 0 for p in facts.prices) # dynamic "-1" prices skipped | ||
| 36 | assert any("hf_repo" in e.identifiers for e in models) | 36 | assert any("hf_repo" in e.identifiers for e in models) | ||
| 37 | assert not any(c.property == "release_date" for c in facts.claims) | 37 | assert not any(c.property == "release_date" for c in facts.claims) | ||
| 38 | + # OpenRouter's own routers are products, never models | ||||
| 39 | + assert not any(e.identifiers.get("openrouter", "").startswith("openrouter/") for e in models) | ||||
| 40 | + routers = [e for e in facts.entities if e.entity_type == "product"] | ||||
| 41 | + assert {e.identifiers["openrouter"] for e in routers} >= {"openrouter/auto", "openrouter/pareto-code", "openrouter/free"} | ||||
| 42 | + auto = next(e for e in routers if e.identifiers["openrouter"] == "openrouter/auto") | ||||
| 43 | + assert auto.name == "Auto Router" and auto.attributes == {"kind": "router"} and auto.organization.name == "OpenRouter" | ||||
| 44 | + assert any(r.predicate == "operates" and r.object is auto for r in facts.relations) | ||||
| 45 | + assert not any(p.provider_model_id == "openrouter/auto" for p in facts.prices) # dynamic (-1) price skipped | ||||
| 46 | + free = next(p for p in facts.prices if p.provider_model_id == "openrouter/free") | ||||
| 47 | + assert free.model.entity_type == "product" and free.input_per_mtok == 0.0 | ||||
| 38 | 48 | ||||
| 39 | 49 | ||||
| 40 | def test_helpers(): | 50 | def test_helpers(): | ||
| 41 | 51 | ||||