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%

Connectors + registry on the canonical ontology (Stream B): artifacts, effort variants as configurations, benchmark families, identity helper

Hugging Face: repositories are models or artifacts (quantization / conversion / packaging) with `canonical` → base model (base_model tag or
analysed name, family org), `family` hints, canonical licence (+license_raw), `access: gated|open` instead of openness=restricted,
weights_available + derived openness, modalities from pipeline tags, parameter_count kept on artifacts only; converters `publish`, never `develop`.
Leaderboards: Artificial Analysis / LiveBench / aider / SWE-bench fold effort variants ("(xhigh)", "-thinking-64k-high-effort", non-reasoning)
into the base model with the setting in config; LiveBench categories become `livebench-<category>` benchmarks; aider well-formed rate →
`aider-polyglot-well-formed`; AA GPQA → gpqa-diamond, τ² → Telecom; trust_level / variant / run_group on every result; bookkeeping out of config;
free-text tags are aliases, identifiers only for trusted pinned vendor ids. New shared `connectors/_identity.py` (provider prefixes, first-party
patterns, effort stripping with size-tier guard, family hints).
Registry: benchmarks gain family / variant / version / family_head / canonical metric / bounds / higher_is_better / harness / comparability_note +
14 entries (gpqa-diamond, aime-2024, swe-bench-pro, arc-agi-2, terminal-bench-2, mmteb, livebench ×7, aider-polyglot-well-formed); seed writes them
and `variant_of` relations, org_kind from `kind`. Organisations: kind + country fields, upstage / stepfun / kwaipilot / kuaishou / thinking-machines /
lg-ai-research / xiaomi / inclusionai entries with hub aliases. Repositories: canonical kinds, agents (agent_kind) and tools get their own entity types.
OpenRouter: prices booked on the OpenRouter provider only (features.upstream_provider), `created` → openrouter_listed_at (no release_date),
canonical modalities, identity helper. Provider pricing: fireworks_model_id (+ legacy scheme), org from the id, family hints.
Labs: no hard-coded Anthropic modalities/tool constants (table-driven), Google pdf → document + google_model_id, canonical licence/status/
capabilities/api_aliases helpers in labs/_common.py, family_model hints everywhere. arXiv/OpenReview: researchers only with an identifier
(ORCID / OpenReview profile). GitHub/PyPI: canonical kind (+kind_raw), licence keys. docs/CONNECTORS.md rewritten on the canonical vocabulary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 12 days ago (Sep 12, 2026) parent 2e6e573

33 changed files +2,331 −442

modified docs/CONNECTORS.md +106 −21
@@ -32,8 +32,7 @@ class ExampleConnector(BaseConnector):
32 32 facts.entities.append(org)
33 33 if target.key == "models" and parsed.html:
34 34 for row in parsed.html.tables[0]["rows"]: ...
35 − model = facts.entity("model", name, identifiers={"example_model_id": api_id}, organization=org)
36 − facts.relate(org, "develops", model)
35 + model = model_ref(facts, name, org, api_id=api_id, provider_key="example") # from connectors/labs/_common.py: identity + family hint
37 36 facts.claim(model, "context_length", 128000, unit="tokens")
38 37 facts.price(model=model, provider=provider_ref("example"), input_per_mtok=1.0, output_per_mtok=4.0)
39 38 facts.follow(detail_url, doc_type="model_page", entity=model, needs_llm=True, meta={"llm_task": "model_passport"})
@@ -56,7 +55,9 @@ CONNECTORS = [ExampleConnector]
56 55
57 56 Helpers: `aiatlas.sdk.extract.numbers` (`parse_param_count("70B")`, `parse_active_params("235B-A22B")`, `parse_context_length("128K")`,
58 57 `parse_money_per_mtok("$3 / 1M tokens")`, `parse_percent`), `aiatlas.sdk.extract.dates` (`parse_datetime`, `parse_date` with precision),
59 −`connectors/labs/_common.py` (`announcement_events`, `transpose_feature_table`, `kv_tables`, `clean_cell`, `money`, `tokens`, `month_year`, `parse_retirement`, `model_ref`).
58 +`connectors/labs/_common.py` (`announcement_events`, `transpose_feature_table`, `kv_tables`, `clean_cell`, `money`, `tokens`, `month_year`, `parse_retirement`,
59 +`model_ref`, `claim_license`, `claim_status`, `claim_modalities`, `claim_api_aliases`, `normalize_capabilities`),
60 +`connectors/_identity.py` (see below).
60 61
61 62 ## Rules
62 63
@@ -64,34 +65,118 @@ Helpers: `aiatlas.sdk.extract.numbers` (`parse_param_count("70B")`, `parse_activ
64 65 `pypi.org/pypi/<pkg>/json`, arXiv Atom) are fine — they are public documents, not commercial APIs. Never require an API key.
65 66 Set `Target(escalate=True)` only for pages known to block bots *and* only if a key is configured; otherwise let the document be `blocked`.
66 67 2. **Never fabricate.** Only emit a claim when the page states it. Unknown → no claim. Don't infer parameter counts from names unless the
67 − name literally contains them (`Qwen3-235B-A22B` → 235e9 / 22e9 is fine; "Large" is not).
68 + name literally contains them (`Qwen3-235B-A22B` → 235e9 / 22e9 is fine; "Large" is not). No hard-coded "all models of this lab do X" constants:
69 + read the comparison table, else omit.
68 70 3. **Identifiers make resolution deterministic.** Give models the provider's API id (`{provider}_model_id`), HF repo (`hf_repo`), arXiv id
69 71 (`arxiv`), GitHub repo (`github_repo`), PyPI name (`pypi`). Organizations always come from `org_ref(<registry key>)` (add missing orgs to
70 − `registry/organizations.yaml` with a `source_url`).
71 −4. **Properties are shared vocabulary** (see below). Add new ones sparingly; prefix metrics with `metric.` (downloads, likes, stars) so they
72 − never generate events.
72 + `registry/organizations.yaml` with a `source_url`) — **never guess a developer from a display name**; the `_identity` helper only maps
73 + first-party prefixes and family words (`claude-` → Anthropic, `gemini-` → Google, `openrouter/x-ai/…` → xAI).
74 +4. **Properties are shared vocabulary** (see below) and **values are canonical**: run licences through `ontology.normalize_license`, statuses through
75 + `normalize_status`, modalities through `normalize_modalities`, capabilities through `_common.normalize_capabilities`; keep the source label in
76 + `<property>_raw` when it differed. Prefix metrics with `metric.` (downloads, likes, stars) so they never generate events.
73 77 5. **Events**: NEW_*, PRICE_CHANGED, CONTEXT_CHANGED… are emitted automatically by the writer. Emit `ANNOUNCEMENT`/`RELEASE` events yourself for
74 78 feed items (`announcement_events`) with `effective_at` = publication date and a `dedupe_key` (URL).
75 79 6. **Follow-ups** (`facts.follow`) let a listing discover detail pages; keep `max_targets` reasonable (`ctx.max_targets`, default 2000).
76 80 7. **Every connector has a fixture test**: save real responses under `tests/fixtures/<connector>/…` and assert extracted facts
77 81 (see `tests/test_anthropic.py`). Run connectors with `--file key=path` to use fixtures instead of the network.
78 82 8. Respect `rate_per_min` from `registry/sources.yaml`; arXiv ≤ 4/min; Hugging Face ≤ 30/min; GitHub ≤ 20/min.
83 +9. **Results carry provenance**: every `ResultObs` sets `trust_level` (`ontology.benchmarks.trust_level(source_key, config)`), `variant`
84 + (GPQA Diamond, SWE-bench Verified, a LiveBench category…) and `run_group` (the evaluation run the row belongs to: LiveBench release,
85 + aider run date, AA index version). Bookkeeping (cost, wall time, command lines, harness versions) stays out of `config`.
86 +
87 +## Identity: model families, models, artifacts, configurations
88 +
89 +The canonical hierarchy is **model_family → model → artifact → deployment** (`aiatlas.ontology.models`). Connectors describe it with hints on
90 +`EntityRef`; the writer materialises `entities.family_id`, `canonical_id`, `artifact_kind`, `identity_confidence` and the relations
91 +`member_of_family` / `artifact_of`.
92 +
93 +| hint | meaning | who sets it |
94 +|---|---|---|
95 +| `family=EntityRef("model_family", "Qwen3.6", organization=org)` | the versioned family the model belongs to (`_identity.family_ref(name, org)` or the lab's own label via `model_ref(family=…)`) | every model ref with a detectable family |
96 +| `canonical=EntityRef("model", …)` | for an **artifact**: the model it packages; for an alias entity: the canonical entity | Hugging Face (quantisations, conversions, mirrors) |
97 +| `artifact_kind` | `checkpoint` \| `quantization` \| `conversion` \| `packaging` | Hugging Face |
98 +| `identity_confidence` | `high` (vendor id / official repo) · `medium` (leaderboard label, analysed base name) · `low` | everyone |
99 +
100 +**Entity types**: `model` (a release: Qwen3-8B, Claude Opus 5), `model_family` (Qwen3, Claude), `artifact` (`bartowski/Qwen3.8-27B-GGUF`,
101 +`zai-org/GLM-5-FP8`, `mlx-community/Kimi-K2.5` — never a model of its own; keeps the full repo id as name, `parameter_count` = the packaged size),
102 +`agent` (coding/browser agents from the repositories registry, attribute `agent_kind`), `tool` (applications, MCP servers), `framework`
103 +(libraries, SDKs, inference engines… attribute `kind` canonical), `repository` (model-code drops), `researcher` (**only with an identifier**:
104 +ORCID or OpenReview profile id — never from a bare author name; authors stay a claim on the paper).
105 +
106 +**Effort variants are configurations, not models.** `claude-opus-5-xhigh`, "GPT-5.5 (xhigh)", `deepseek-v3-1-reasoning`,
107 +`…-thinking-64k-high-effort`, "Mistral Small 4 (Non-reasoning)" all point at the base model; the setting goes into `ResultObs.config`
108 +(`reasoning_effort`, `reasoning: on|off|adaptive`, `thinking_budget`) and the evaluator's slug into `config.aa_slug` / `livebench_model_id`.
109 +Size tiers and official products are not efforts: `mistral-medium`, `qwen3.7-max`, `sonar-reasoning`, `kimi-k2-thinking`, `grok-4-1-fast` stay models
110 +(`_identity.strip_effort` only strips an ambiguous suffix when the stem still carries a version digit).
111 +
112 +### `connectors/_identity.py`
113 +
114 +`model_identity(api_id, trusted=False)` analyses an API id from a third-party source (`anthropic/claude-3-7-sonnet-20250219`,
115 +`gemini/gemini-2.5-pro-preview-05-06`, `openrouter/x-ai/grok-4`, `gpt-4o-2024-08-06`, `Qwen/Qwen2.5-Coder-32B-Instruct`,
116 +`fireworks_ai/accounts/fireworks/models/qwq-32b`) and returns the developer organisation key, resolver-friendly aliases (raw id, id without
117 +provider prefix, id without effort suffixes), the effort configuration and — **only when `trusted=True`** (the string really is the vendor's own
118 +API id, e.g. the `--model` argument aider passed) — the vendor identifier (`anthropic_model_id`, `gemini_model_id`, `openai_model_id`,
119 +`xai_model_id`, `deepseek_model_id`, `mistral_model_id`, `cohere_model_id`, `hf_repo`, `openrouter`, `fireworks_model_id`). Free-text tags
120 +(SWE-bench "Model:" tags, LiveBench ids, OpenRouter slugs) never become identifiers: the resolver refuses an alias match when the entity already
121 +carries a *different* value for the same scheme, so a wrong id would split an entity instead of linking it. Rolling ids
122 +(`deepseek/deepseek-chat`, `codestral-latest`) are aliases only even when trusted — they name whichever snapshot served that day.
123 +`model_ref_from_api_id(facts, api_id, name=…, trusted=…)` builds the deduplicated model `EntityRef` (+ family hint) and returns the effort config.
124 +`family_ref`, `split_effort_label`, `strip_effort`, `org_key_for_vendor`, `org_ref_in` are the building blocks.
79 125
80 126 ## Property vocabulary (entities.attributes)
81 127
82 −**model**: `family, version, release_date (ISO, may be YYYY-MM), status (active|preview|deprecated|retired|announced|limited-availability), openness
83 −(open-weights|open-source|proprietary|restricted), license, architecture, parameter_count (int), active_parameter_count, is_moe, modalities
84 −(list of text|image|audio|video|code|embedding), modalities_input, modalities_output, context_length (tokens), max_output_tokens, knowledge_cutoff
85 −(YYYY-MM), training_data_cutoff, languages, tool_calling, structured_output, reasoning, vision, audio, fine_tuning_available, tokenizer,
86 −api_model_id, api_alias, official_url, model_card_url, paper_url, repository_url, hf_repo, pipeline_tag, base_model, quantization, quant_format
87 −(gguf|mlx|awq|gptq|fp8), file_size_gb, deprecation_date, retirement_date, retirement_tentative, metric.downloads, metric.likes`
88 −
89 −**company / organization / lab**: `country (ISO-2), headquarters, founded, website, domains, hf_org, github_org, org_kind, legal_name, founders, leadership, employee_count`
128 +Canonical enums live in `src/aiatlas/ontology/` — connectors write canonical values and keep the source label in `<property>_raw`.
129 +
130 +**model**: `family` (label of the `model_family` hint), `version, release_date (ISO, may be YYYY-MM), status` (`ontology.taxonomy.MODEL_STATUSES`:
131 +`announced | preview | active | limited-availability | deprecated | retired | archived | unknown`), `openness` (`ontology.openness.OPENNESS_CATEGORIES`:
132 +`open-source | open-weights | restricted-weights | proprietary | unknown` — **derived** with `derive_openness` from `weights_available` + the licence,
133 +asserted directly only when the source literally states it), `weights_available` (bool), `access` (`gated | open`, Hugging Face gating — access,
134 +not a licence property), `gated_mode`, `license` (`ontology.licenses` key: `Apache-2.0`, `MIT`, `Llama-3.1-Community`, `Mistral-Research`,
135 +`Gemma-Terms`… — SPDX id when one exists) + `license_raw`, `architecture, parameter_count (int), active_parameter_count, is_moe, modalities /
136 +modalities_input / modalities_output` (`ontology.taxonomy.MODALITIES`: `text | image | audio | video | document | code | embedding | 3d | structured | action`,
137 +sorted), `context_length (tokens), max_output_tokens, knowledge_cutoff (YYYY-MM), training_data_cutoff, languages, capabilities` (canonical slugs:
138 +`function_calling, structured_output, reasoning, vision, audio_input, audio_output, image_generation, video_generation, code_execution,
139 +search_grounding, caching, batch, fine_tuning, streaming, live_api, computer_use, file_search, url_context, mcp…` via `_common.normalize_capabilities`)
140 ++ `capabilities_raw`, `tool_calling, structured_output, reasoning, vision, audio, fine_tuning_available, tokenizer, api_model_id, api_aliases` (always a
141 +list; `api_alias` = the first one, kept for compatibility), `official_url, model_card_url, paper_url, repository_url, hf_repo, pipeline_tag, base_model,
142 +quantization, quant_format (gguf|mlx|awq|gptq|fp8|…), artifact_kind, is_quantized, quantized_by, file_size_gb, deprecation_date, retirement_date,
143 +retirement_tentative, openrouter_listed_at` (listing date — never `release_date`), `aa_release_date / aa_openness / aa_context_window` (second-hand
144 +facts from Artificial Analysis), `metric.downloads, metric.likes`
145 +
146 +**company / organization / lab**: `country (ISO-2), headquarters, founded, website, domains, hf_org, github_org, org_kind`
147 +(`ontology.taxonomy.ORG_KINDS`: `company | lab | university | nonprofit | government | community | consortium | individual`), `legal_name, founders, leadership, employee_count`
90 148
91 149 **provider**: `website, pricing_url, docs_url, regions, features` · **paper**: `authors, published_at, updated_at, abstract, arxiv_id, doi, categories, primary_category, pdf_url, code_url, venue`
92 −· **benchmark**: `category, task, metric, unit, creator, website, paper, known_limitations, methodology` · **hardware**: `kind, architecture, release_date, memory_gb,
93 −memory_type, memory_bandwidth_gbs, tdp_watts, runtimes, manufacturer, spec_url, price_usd, compute_fp16_tflops` · **framework / repository**: `repository_url, latest_version,
94 −latest_release_at, license, language, description, topics, pypi, metric.stars, metric.forks` · **dataset**: `license, modality, size, publisher, task, hf_repo`
95 −
96 −Relations: `develops, owns, operates, available_through, evaluated_on, described_by, derived_from, fine_tuned_from, quantized_from, distilled_from,
97 −merged_from, superseded_by, runs_on, uses, manufactures, funded_by, acquired, authored, works_at, uses_dataset, evaluates_on, integrates, published_by`.
150 +· **researcher**: `openreview_profile_url` (+ identifiers `orcid` / `openreview_profile`)
151 +· **benchmark** (registry `registry/benchmarks.yaml` + `.d/`, see below): `family, variant, version, family_head, category, task, metric` (canonical:
152 +`accuracy | pass@1 | pass^1 | resolved | pass_rate_2 | percent_cases_well_formed | global_average | average score | mean score | index | elo …`),
153 +`metric_label, metric_raw, unit, metric_min, metric_max, higher_is_better, harness, comparability_note, creator, website, paper, known_limitations, methodology`
154 +· **hardware**: `kind` (`ontology.taxonomy.HARDWARE_KINDS`), `architecture, release_date, memory_gb, memory_type, memory_bandwidth_gbs, tdp_watts, runtimes, manufacturer, spec_url,
155 +price_usd, compute_fp16_tflops` · **framework / tool / agent / repository**: `kind` (`ontology.taxonomy.FRAMEWORK_KINDS`: `training-framework | inference-engine |
156 +serving-engine | library | runtime | agent-framework | orchestration | evaluation-harness | sdk | tool | application | agent | mcp-server | vector-database |
157 +observability | data-tooling`) + `kind_raw`, `agent_kind` (`coding | browser | research`), `repository_url, latest_version, latest_release_at, license (+ license_raw),
158 +language, description, topics, pypi, metric.stars, metric.forks` · **dataset**: `license, modality, size, publisher, task, hf_repo`
159 +
160 +Relations: `develops, published_by` (artifact → its publishing org; converters never `develop`), `owns, operates, available_through, evaluated_on,
161 +described_by, derived_from, fine_tuned_from, quantized_from, distilled_from, merged_from, superseded_by, variant_of` (benchmark variant → family head),
162 +`runs_on, uses, manufactures, funded_by, acquired, authored, works_at, uses_dataset, evaluates_on, integrates`. The writer adds `member_of_family` and
163 +`artifact_of` from the hints.
164 +
165 +## Prices
166 +
167 +`PriceObs(model, provider, provider_model_id, …)` — one row per model × provider × provider id. Aggregator prices belong to the aggregator:
168 +OpenRouter rows are booked on `provider_ref("openrouter")` with `features.upstream_provider` = the routed lab's registry provider key (never on
169 +the lab's own provider entity, which would create two "current" prices). Variants (`:free`, `:thinking`, `:nitro`) are separate rows of the same
170 +model (`features.variant`). Identifier schemes for provider catalogues: `openrouter`, `groq_model_id`, `together_ai_model_slug`,
171 +`fireworks_model_id` (the historical `fireworks-ai_model_id` is still emitted alongside).
172 +
173 +## Benchmark registry (`registry/benchmarks.yaml` + `registry/benchmarks.d/*.yaml`)
174 +
175 +Every entry declares `family`, `variant`, optional `version`, `family_head: true` on the representative member, canonical `metric`
176 +(`ontology.benchmarks.normalize_metric`) with `metric_min` / `metric_max` / `higher_is_better`, `harness` and `comparability_note`, plus the
177 +aliases evaluators use ("GPQA Diamond", "HLE", "SWE-Bench Verified", "τ²-Bench Telecom"…). `aia seed` writes them as claims and a `variant_of`
178 +relation from each member to its family head. One benchmark entity per measured thing: LiveBench categories are `livebench-<category>` entities
179 +(family `livebench`), aider's well-formed rate is `aider-polyglot-well-formed`, Artificial Analysis' GPQA results land on `gpqa-diamond`.
180 +Connectors address benchmarks with `benchmark_ref(key)` (identifier `registry_benchmark`, `slug_hint` = key). Slugs are only hints: when an
181 +organisation already owns the slug (`livebench` the org), the resolver appends a collision suffix (`livebench-2` in production) — identity is
182 +the `registry_benchmark` identifier, and existing production slugs never change.
modified registry/benchmarks.d/ecosystem.yaml +110 −10
@@ -1,81 +1,181 @@
1 −# Benchmarks reported by the leaderboard connectors (SWE-bench boards, Artificial Analysis component evaluations). Fragment of registry/benchmarks.yaml.
1 +# Benchmarks reported by the leaderboard connectors (SWE-bench boards, Artificial Analysis component evaluations). Fragment of registry/benchmarks.yaml
2 +# (same canonical fields: family / variant / version / family_head / metric / metric_min / metric_max / higher_is_better / harness / comparability_note).
2 3 benchmarks:
3 − - key: swe-bench-lite
4 − name: SWE-bench Lite
4 + - key: swe-bench-full
5 + name: SWE-bench (full test split)
6 + aliases: [SWE-bench Test, SWE-bench Full, SWE-bench (full), swebench full]
7 + family: swe-bench
8 + variant: full
9 + family_head: true
5 10 category: coding
6 − task: resolve real GitHub issues (300-instance subset)
11 + task: resolve real GitHub issues (2,294 instances)
7 12 metric: resolved
13 + metric_label: "% resolved"
8 14 unit: "%"
15 + metric_min: 0
16 + metric_max: 100
17 + higher_is_better: true
18 + harness: submitter's agent scaffold (config.system); evaluation by the SWE-bench harness
19 + comparability_note: Scaffold-dependent; compare within one scaffold and attempt regime only.
9 20 website: https://www.swebench.com
10 21 paper: https://arxiv.org/abs/2310.06770
11 − known_limitations: Scaffold/agent dependent; results are not comparable across harnesses.
12 22 source_url: https://www.swebench.com/
13 − - key: swe-bench-full
14 − name: SWE-bench (full test split)
15 − aliases: [SWE-bench Test, SWE-bench Full]
23 + - key: swe-bench-lite
24 + name: SWE-bench Lite
25 + aliases: [SWE-Bench Lite, swebench lite]
26 + family: swe-bench
27 + variant: Lite
16 28 category: coding
17 − task: resolve real GitHub issues (2,294 instances)
29 + task: resolve real GitHub issues (300-instance subset)
18 30 metric: resolved
31 + metric_label: "% resolved"
19 32 unit: "%"
33 + metric_min: 0
34 + metric_max: 100
35 + higher_is_better: true
36 + harness: submitter's agent scaffold (config.system); evaluation by the SWE-bench harness
37 + comparability_note: Scaffold-dependent; Lite is an easier subset — never compare with Verified or full scores.
20 38 website: https://www.swebench.com
21 39 paper: https://arxiv.org/abs/2310.06770
40 + known_limitations: Scaffold/agent dependent; results are not comparable across harnesses.
22 41 source_url: https://www.swebench.com/
23 42 - key: swe-bench-multimodal
24 43 name: SWE-bench Multimodal
44 + aliases: [SWE-Bench Multimodal, SWE-bench MM]
45 + family: swe-bench
46 + variant: Multimodal
25 47 category: coding
26 48 task: resolve visual JavaScript issues (screenshots + code)
27 49 metric: resolved
50 + metric_label: "% resolved"
28 51 unit: "%"
52 + metric_min: 0
53 + metric_max: 100
54 + higher_is_better: true
55 + harness: submitter's agent scaffold (config.system); evaluation by the SWE-bench harness
56 + comparability_note: Requires image input; scaffold-dependent.
29 57 website: https://www.swebench.com/multimodal
30 58 source_url: https://www.swebench.com/
31 59 - key: swe-bench-multilingual
32 60 name: SWE-bench Multilingual
61 + aliases: [SWE-Bench Multilingual]
62 + family: swe-bench
63 + variant: Multilingual
33 64 category: coding
34 65 task: resolve GitHub issues across 9 programming languages
35 66 metric: resolved
67 + metric_label: "% resolved"
36 68 unit: "%"
69 + metric_min: 0
70 + metric_max: 100
71 + higher_is_better: true
72 + harness: submitter's agent scaffold (config.system); evaluation by the SWE-bench harness
73 + comparability_note: Scaffold-dependent; different instance set from Verified.
37 74 website: https://www.swebench.com/multilingual
38 75 source_url: https://www.swebench.com/
76 + - key: swe-bench-pro
77 + name: SWE-Bench Pro
78 + aliases: [SWE-bench Pro, SWE Bench Pro, swebench pro]
79 + family: swe-bench
80 + variant: Pro
81 + category: coding
82 + task: long-horizon, enterprise-grade software engineering tasks (public set) curated by Scale AI
83 + metric: resolved
84 + metric_label: "% resolved"
85 + unit: "%"
86 + metric_min: 0
87 + metric_max: 100
88 + higher_is_better: true
89 + harness: Scale AI evaluation with a fixed scaffold per leaderboard column
90 + comparability_note: Public vs commercial (held-out) sets are different leaderboards; not comparable with SWE-bench Verified.
91 + website: https://scale.com/leaderboard/swe_bench_pro_public
92 + source_url: https://scale.com/leaderboard/swe_bench_pro_public
39 93 - key: mmmu-pro
40 94 name: MMMU-Pro
95 + aliases: [MMMU Pro]
96 + family: mmmu
97 + variant: Pro
41 98 category: multimodal
42 99 task: robust multimodal understanding (10-option, vision-only variants)
43 100 metric: accuracy
101 + metric_label: accuracy
44 102 unit: "%"
103 + metric_min: 0
104 + metric_max: 100
105 + higher_is_better: true
106 + harness: Artificial Analysis runs its own evaluation; labs self-report
107 + comparability_note: Standard (10-option) vs vision-only settings are different numbers.
45 108 paper: https://arxiv.org/abs/2409.02813
46 109 source_url: https://arxiv.org/abs/2409.02813
47 110 - key: livecodebench
48 111 name: LiveCodeBench
112 + aliases: [LCB, Live Code Bench]
113 + family: livecodebench
114 + variant: rolling
115 + family_head: true
49 116 category: coding
50 117 task: contamination-free competitive programming problems
51 118 metric: pass@1
119 + metric_label: pass@1
52 120 unit: "%"
121 + metric_min: 0
122 + metric_max: 100
123 + higher_is_better: true
124 + harness: Artificial Analysis runs its own evaluation; labs self-report on a chosen date window
125 + comparability_note: The problem window (release dates) differs between reporters — scores from different windows are not comparable.
53 126 website: https://livecodebench.github.io
54 127 paper: https://arxiv.org/abs/2403.07974
55 128 source_url: https://arxiv.org/abs/2403.07974
56 129 - key: scicode
57 130 name: SciCode
131 + aliases: [SciCode benchmark]
132 + family: scicode
133 + variant: main
134 + family_head: true
58 135 category: coding
59 136 task: research-level scientific coding problems
60 137 metric: accuracy
138 + metric_label: accuracy
61 139 unit: "%"
140 + metric_min: 0
141 + metric_max: 100
142 + higher_is_better: true
143 + harness: Artificial Analysis runs its own evaluation
144 + comparability_note: Sub-problem vs main-problem accuracy are different numbers; with/without background prompts.
62 145 website: https://scicode-bench.github.io
63 146 paper: https://arxiv.org/abs/2407.13168
64 147 source_url: https://arxiv.org/abs/2407.13168
65 148 - key: ifbench
66 149 name: IFBench
150 + aliases: [IF-Bench]
151 + family: ifeval
152 + variant: IFBench
67 153 category: instruction-following
68 154 task: precise instruction following with novel constraints
69 155 metric: accuracy
156 + metric_label: accuracy
70 157 unit: "%"
158 + metric_min: 0
159 + metric_max: 100
160 + higher_is_better: true
161 + harness: Artificial Analysis runs its own evaluation
162 + comparability_note: Different constraint set from IFEval; not comparable with IFEval scores.
71 163 paper: https://arxiv.org/abs/2507.02833
72 164 source_url: https://arxiv.org/abs/2507.02833
73 165 - key: tau2-bench
74 166 name: τ²-bench
75 − aliases: [tau2-bench, TAU2-bench, tau-squared bench]
167 + aliases: [tau2-bench, TAU2-bench, tau-squared bench, τ²-Bench, τ2-bench, τ²-Bench Telecom, tau2 bench telecom]
168 + family: tau-bench
169 + variant: τ²
76 170 category: agentic
77 171 task: dual-control tool-agent-user interaction (telecom, retail, airline)
78 172 metric: pass^1
173 + metric_label: pass^1
79 174 unit: "%"
175 + metric_min: 0
176 + metric_max: 100
177 + higher_is_better: true
178 + harness: simulated user (LLM) + dual-control tool environment; Artificial Analysis reports the Telecom domain
179 + comparability_note: Domain (config.variant = Telecom / Retail / Airline) and the user-simulator model must match.
80 180 paper: https://arxiv.org/abs/2506.07982
81 181 source_url: https://arxiv.org/abs/2506.07982
modified registry/benchmarks.yaml +423 −20
@@ -1,12 +1,29 @@
1 1 # Benchmark definitions (entities of type `benchmark`). Results come from connectors; definitions are curated with sources.
2 +#
3 +# Canonical fields (see aiatlas.ontology.benchmarks):
4 +# family benchmark family key (swe-bench, gpqa, livebench…); `variant` names the member, `version` the dataset revision when one exists
5 +# family_head true on the representative entry of a family — every other member gets a `variant_of` relation to it (aia seed)
6 +# metric canonical metric key from ontology.benchmarks.METRICS (accuracy, pass@1, resolved, elo, index…); `metric_label` = human label
7 +# higher_is_better score direction · metric_min / metric_max = bounds of the reported metric (null = unbounded)
8 +# harness who runs the evaluation / with what scaffold; comparability_note = what makes two scores on this board (not) comparable
9 +# aliases labels used by Artificial Analysis, LiveBench, model cards, papers… (the API resolves benchmarks by alias)
2 10 benchmarks:
3 11 - key: mmlu
4 12 name: MMLU
5 − aliases: [Massive Multitask Language Understanding]
13 + aliases: [Massive Multitask Language Understanding, MMLU 5-shot, MMLU (5-shot)]
14 + family: mmlu
15 + variant: original
16 + family_head: true
6 17 category: knowledge
7 18 task: 57-subject multiple-choice questions
8 19 metric: accuracy
20 + metric_label: accuracy
9 21 unit: "%"
22 + metric_min: 0
23 + metric_max: 100
24 + higher_is_better: true
25 + harness: self-reported by labs (shot count and prompt format vary)
26 + comparability_note: Widespread training-data contamination; 0-shot vs 5-shot and chain-of-thought prompting change scores by several points.
10 27 creator: Hendrycks et al.
11 28 website: https://github.com/hendrycks/test
12 29 paper: https://arxiv.org/abs/2009.03300
@@ -14,160 +31,546 @@ benchmarks:
14 31 source_url: https://arxiv.org/abs/2009.03300
15 32 - key: mmlu-pro
16 33 name: MMLU-Pro
34 + aliases: [MMLU Pro, MMLU-Pro (0-shot CoT)]
35 + family: mmlu
36 + variant: Pro
17 37 category: knowledge
18 38 task: harder, 10-option MMLU variant
19 39 metric: accuracy
40 + metric_label: accuracy
20 41 unit: "%"
42 + metric_min: 0
43 + metric_max: 100
44 + higher_is_better: true
45 + harness: self-reported by labs or independent evaluators
46 + comparability_note: 10-option questions; chain-of-thought is the standard regime, direct-answer scores are lower and not comparable.
21 47 website: https://github.com/TIGER-AI-Lab/MMLU-Pro
22 48 paper: https://arxiv.org/abs/2406.01574
23 49 source_url: https://arxiv.org/abs/2406.01574
24 50 - key: gpqa
25 51 name: GPQA
26 − aliases: [GPQA Diamond]
52 + aliases: [GPQA main, Graduate-Level Google-Proof Q&A]
53 + family: gpqa
54 + variant: main
55 + family_head: true
27 56 category: reasoning
28 − task: graduate-level science questions
57 + task: graduate-level science questions (main set, 448 questions)
29 58 metric: accuracy
59 + metric_label: accuracy
30 60 unit: "%"
61 + metric_min: 0
62 + metric_max: 100
63 + higher_is_better: true
64 + harness: self-reported by labs or independent evaluators
65 + comparability_note: Main and Diamond subsets are different question sets; reasoning effort and sampling (pass@1 vs majority vote) change scores.
66 + website: https://github.com/idavidrein/gpqa
67 + paper: https://arxiv.org/abs/2311.12022
68 + source_url: https://arxiv.org/abs/2311.12022
69 + - key: gpqa-diamond
70 + name: GPQA Diamond
71 + aliases: [GPQA-Diamond, gpqa_diamond, GPQA (Diamond), GPQA Diamond (0-shot)]
72 + family: gpqa
73 + variant: Diamond
74 + category: reasoning
75 + task: graduate-level science questions — the 198-question Diamond subset (expert-validated, non-expert-failed)
76 + metric: accuracy
77 + metric_label: accuracy
78 + unit: "%"
79 + metric_min: 0
80 + metric_max: 100
81 + higher_is_better: true
82 + harness: Artificial Analysis runs its own evaluation; labs self-report with varying sampling
83 + comparability_note: 198 questions → ±3 pt noise; reasoning effort, thinking budget and repeated sampling strongly affect scores.
31 84 website: https://github.com/idavidrein/gpqa
32 85 paper: https://arxiv.org/abs/2311.12022
33 86 source_url: https://arxiv.org/abs/2311.12022
34 87 - key: humaneval
35 88 name: HumanEval
89 + aliases: [HumanEval pass@1, OpenAI HumanEval]
90 + family: humaneval
91 + variant: original
92 + family_head: true
36 93 category: coding
37 94 task: Python function synthesis from docstrings
38 95 metric: pass@1
96 + metric_label: pass@1
39 97 unit: "%"
98 + metric_min: 0
99 + metric_max: 100
100 + higher_is_better: true
101 + harness: self-reported by labs
102 + comparability_note: Saturated; pass@1 vs pass@k and prompt wrapping differ across reporters.
40 103 website: https://github.com/openai/human-eval
41 104 paper: https://arxiv.org/abs/2107.03374
42 105 known_limitations: Saturated; small (164 problems).
43 106 source_url: https://arxiv.org/abs/2107.03374
44 107 - key: swe-bench-verified
45 108 name: SWE-bench Verified
46 − aliases: [SWE-bench]
109 + aliases: [SWE-bench, SWE-Bench Verified, SWE-bench-Verified, swebench verified, SWE Bench Verified]
110 + family: swe-bench
111 + variant: Verified
47 112 category: coding
48 113 task: resolve real GitHub issues (500 human-validated instances)
49 114 metric: resolved
115 + metric_label: "% resolved"
50 116 unit: "%"
117 + metric_min: 0
118 + metric_max: 100
119 + higher_is_better: true
120 + harness: submitter's agent scaffold (config.system); evaluation by the SWE-bench harness, submissions optionally checked by the SWE-bench team
121 + comparability_note: Scores depend on the agent scaffold as much as on the model; compare only within one scaffold (config.system) and one attempt regime.
51 122 website: https://www.swebench.com
52 123 paper: https://arxiv.org/abs/2310.06770
53 124 known_limitations: Scaffold/agent dependent; results are not comparable across harnesses.
54 125 source_url: https://www.swebench.com/
55 126 - key: aider-polyglot
56 127 name: Aider polyglot
57 − aliases: [Aider polyglot coding leaderboard]
128 + aliases: [Aider polyglot coding leaderboard, Aider Polyglot, aider polyglot benchmark, Aider]
129 + family: aider-polyglot
130 + variant: polyglot
131 + family_head: true
58 132 category: coding
59 133 task: 225 Exercism exercises in 6 languages, edit-format aware
60 − metric: pass rate (2 attempts)
134 + metric: pass_rate_2
135 + metric_label: pass rate (2 attempts)
61 136 unit: "%"
137 + metric_min: 0
138 + metric_max: 100
139 + higher_is_better: true
140 + harness: aider (config.edit_format = whole | diff | diff-fenced | architect), second attempt after test feedback
141 + comparability_note: Edit format and aider version change results; the well-formed rate is a separate benchmark entry.
62 142 website: https://aider.chat/docs/leaderboards/
63 143 known_limitations: Depends on aider's edit format and prompting; cost column depends on provider pricing.
64 144 source_url: https://aider.chat/docs/leaderboards/
145 + - key: aider-polyglot-well-formed
146 + name: Aider polyglot — well-formed responses
147 + aliases: [Aider well-formed rate, percent_cases_well_formed, Aider polyglot well-formed]
148 + family: aider-polyglot
149 + variant: well-formed
150 + category: coding
151 + task: share of aider polyglot cases where every edit was syntactically well formed (parsable edit blocks)
152 + metric: percent_cases_well_formed
153 + metric_label: "% cases well formed"
154 + unit: "%"
155 + metric_min: 0
156 + metric_max: 100
157 + higher_is_better: true
158 + harness: aider (config.edit_format), same runs as aider-polyglot
159 + comparability_note: Measures edit-format compliance, not correctness; only comparable within one edit format.
160 + website: https://aider.chat/docs/leaderboards/
161 + source_url: https://aider.chat/docs/leaderboards/
65 162 - key: livebench
66 163 name: LiveBench
164 + aliases: [LiveBench global average, LiveBench Global, LiveBench overall]
165 + family: livebench
166 + variant: global
167 + family_head: true
67 168 category: general
68 − task: contamination-limited, monthly refreshed questions across 6 categories
69 − metric: average score
169 + task: contamination-limited, monthly refreshed questions across 7 categories — overall mean of the category averages
170 + metric: global_average
171 + metric_label: global average
70 172 unit: "%"
173 + metric_min: 0
174 + metric_max: 100
175 + higher_is_better: true
176 + harness: LiveBench official runs; config.release = question-set release date
177 + comparability_note: Each release is a new question set — compare scores within one release (run group) only; effort variants are configurations.
71 178 website: https://livebench.ai
72 179 paper: https://arxiv.org/abs/2406.19314
73 180 source_url: https://livebench.ai/
181 + - key: livebench-reasoning
182 + name: LiveBench Reasoning
183 + aliases: [LiveBench — Reasoning, LiveBench reasoning average]
184 + family: livebench
185 + variant: Reasoning
186 + category: reasoning
187 + task: LiveBench reasoning category — mean of its subtasks (web of lies, zebra puzzles, spatial…)
188 + metric: average score
189 + metric_label: category average
190 + unit: "%"
191 + metric_min: 0
192 + metric_max: 100
193 + higher_is_better: true
194 + harness: LiveBench official runs
195 + comparability_note: Compare within one release only; subtask mix changes between releases.
196 + website: https://livebench.ai
197 + source_url: https://livebench.ai/
198 + - key: livebench-coding
199 + name: LiveBench Coding
200 + aliases: [LiveBench — Coding, LiveBench coding average]
201 + family: livebench
202 + variant: Coding
203 + category: coding
204 + task: LiveBench coding category — mean of its subtasks (LCB generation, code completion)
205 + metric: average score
206 + metric_label: category average
207 + unit: "%"
208 + metric_min: 0
209 + metric_max: 100
210 + higher_is_better: true
211 + harness: LiveBench official runs
212 + comparability_note: Compare within one release only; subtask mix changes between releases.
213 + website: https://livebench.ai
214 + source_url: https://livebench.ai/
215 + - key: livebench-agentic-coding
216 + name: LiveBench Agentic Coding
217 + aliases: [LiveBench — Agentic Coding, LiveBench agentic coding average]
218 + family: livebench
219 + variant: Agentic Coding
220 + category: coding
221 + task: LiveBench agentic coding category — mean of its subtasks
222 + metric: average score
223 + metric_label: category average
224 + unit: "%"
225 + metric_min: 0
226 + metric_max: 100
227 + higher_is_better: true
228 + harness: LiveBench official runs
229 + comparability_note: Compare within one release only; subtask mix changes between releases.
230 + website: https://livebench.ai
231 + source_url: https://livebench.ai/
232 + - key: livebench-mathematics
233 + name: LiveBench Mathematics
234 + aliases: [LiveBench — Mathematics, LiveBench math average, LiveBench Math]
235 + family: livebench
236 + variant: Mathematics
237 + category: math
238 + task: LiveBench mathematics category — mean of its subtasks (competition math, AMPS hard, olympiad)
239 + metric: average score
240 + metric_label: category average
241 + unit: "%"
242 + metric_min: 0
243 + metric_max: 100
244 + higher_is_better: true
245 + harness: LiveBench official runs
246 + comparability_note: Compare within one release only; subtask mix changes between releases.
247 + website: https://livebench.ai
248 + source_url: https://livebench.ai/
249 + - key: livebench-data-analysis
250 + name: LiveBench Data Analysis
251 + aliases: [LiveBench — Data Analysis, LiveBench data analysis average]
252 + family: livebench
253 + variant: Data Analysis
254 + category: general
255 + task: LiveBench data analysis category — mean of its subtasks (table reformatting, join, column type)
256 + metric: average score
257 + metric_label: category average
258 + unit: "%"
259 + metric_min: 0
260 + metric_max: 100
261 + higher_is_better: true
262 + harness: LiveBench official runs
263 + comparability_note: Compare within one release only; subtask mix changes between releases.
264 + website: https://livebench.ai
265 + source_url: https://livebench.ai/
266 + - key: livebench-language
267 + name: LiveBench Language
268 + aliases: [LiveBench — Language, LiveBench language average]
269 + family: livebench
270 + variant: Language
271 + category: general
272 + task: LiveBench language category — mean of its subtasks (typos, connections, plot unscrambling)
273 + metric: average score
274 + metric_label: category average
275 + unit: "%"
276 + metric_min: 0
277 + metric_max: 100
278 + higher_is_better: true
279 + harness: LiveBench official runs
280 + comparability_note: Compare within one release only; subtask mix changes between releases.
281 + website: https://livebench.ai
282 + source_url: https://livebench.ai/
283 + - key: livebench-if
284 + name: LiveBench Instruction Following
285 + aliases: [LiveBench — IF, LiveBench IF, LiveBench instruction following average]
286 + family: livebench
287 + variant: IF
288 + category: instruction-following
289 + task: LiveBench instruction-following category — mean of its subtasks (paraphrase, simplify, story generation, summarize)
290 + metric: average score
291 + metric_label: category average
292 + unit: "%"
293 + metric_min: 0
294 + metric_max: 100
295 + higher_is_better: true
296 + harness: LiveBench official runs
297 + comparability_note: Compare within one release only; subtask mix changes between releases.
298 + website: https://livebench.ai
299 + source_url: https://livebench.ai/
74 300 - key: math-500
75 301 name: MATH-500
76 − aliases: [MATH]
302 + aliases: [MATH, MATH 500, MATH500]
303 + family: math
304 + variant: "500"
305 + family_head: true
77 306 category: math
78 − task: competition mathematics
307 + task: competition mathematics (500-problem subset of MATH used since the PRM800K paper)
79 308 metric: accuracy
309 + metric_label: accuracy
80 310 unit: "%"
311 + metric_min: 0
312 + metric_max: 100
313 + higher_is_better: true
314 + harness: self-reported by labs or independent evaluators
315 + comparability_note: Saturated at the frontier; sampling regime (pass@1 vs majority vote) must match.
81 316 paper: https://arxiv.org/abs/2103.03874
82 317 source_url: https://arxiv.org/abs/2103.03874
318 + - key: aime-2024
319 + name: AIME 2024
320 + aliases: [AIME24, AIME 24, AIME I/II 2024]
321 + family: aime
322 + variant: "2024"
323 + version: "2024"
324 + category: math
325 + task: American Invitational Mathematics Examination 2024 problems (30 problems, I + II)
326 + metric: accuracy
327 + metric_label: accuracy
328 + unit: "%"
329 + metric_min: 0
330 + metric_max: 100
331 + higher_is_better: true
332 + harness: self-reported by labs or independent evaluators
333 + comparability_note: 30 problems → high variance; pass@1 averaged over samples vs majority voting (cons@64) are not comparable; 2024 problems appear in training data of later models.
334 + source_url: https://maa.org/maa-invitational-competitions/
83 335 - key: aime-2025
84 336 name: AIME 2025
85 − aliases: [AIME]
337 + aliases: [AIME, AIME25, AIME 25, AIME I/II 2025]
338 + family: aime
339 + variant: "2025"
340 + version: "2025"
341 + family_head: true
86 342 category: math
87 − task: American Invitational Mathematics Examination problems
343 + task: American Invitational Mathematics Examination 2025 problems (30 problems, I + II)
88 344 metric: accuracy
345 + metric_label: accuracy
89 346 unit: "%"
347 + metric_min: 0
348 + metric_max: 100
349 + higher_is_better: true
350 + harness: Artificial Analysis runs its own evaluation; labs self-report with varying sampling
351 + comparability_note: 30 problems → high variance; pass@1 averaged over samples vs majority voting are not comparable; reasoning effort matters.
90 352 known_limitations: 30 problems per year; high variance; often reported with majority voting.
91 353 source_url: https://maa.org/maa-invitational-competitions/
92 354 - key: arc-agi
93 355 name: ARC-AGI
94 − aliases: [ARC-AGI-1, ARC-AGI-2, Abstraction and Reasoning Corpus]
356 + aliases: [ARC-AGI-1, ARC AGI 1, Abstraction and Reasoning Corpus, ARC-AGI v1]
357 + family: arc-agi
358 + variant: "1"
359 + version: "1"
360 + family_head: true
95 361 category: reasoning
96 − task: novel visual abstraction puzzles
362 + task: novel visual abstraction puzzles (ARC-AGI-1 semi-private / public evaluation sets)
97 363 metric: accuracy
364 + metric_label: accuracy
98 365 unit: "%"
366 + metric_min: 0
367 + metric_max: 100
368 + higher_is_better: true
369 + harness: ARC Prize Foundation verified runs (semi-private set) or self-reported public-set runs
370 + comparability_note: Public, semi-private and private evaluation sets differ; the ARC Prize reports cost per task alongside the score.
99 371 website: https://arcprize.org
100 372 source_url: https://arcprize.org/
373 + - key: arc-agi-2
374 + name: ARC-AGI-2
375 + aliases: [ARC AGI 2, ARC-AGI v2, ARC-AGI 2]
376 + family: arc-agi
377 + variant: "2"
378 + version: "2"
379 + category: reasoning
380 + task: harder abstraction puzzles released in 2025 (ARC-AGI-2), designed to resist brute-force program search
381 + metric: accuracy
382 + metric_label: accuracy
383 + unit: "%"
384 + metric_min: 0
385 + metric_max: 100
386 + higher_is_better: true
387 + harness: ARC Prize Foundation verified runs (semi-private set) or self-reported public-set runs
388 + comparability_note: Not comparable with ARC-AGI-1 scores; evaluation set (public / semi-private) and cost budget must match.
389 + website: https://arcprize.org/arc-agi/2/
390 + source_url: https://arcprize.org/arc-agi/2/
101 391 - key: humanitys-last-exam
102 392 name: Humanity's Last Exam
103 − aliases: [HLE]
393 + aliases: [HLE, Humanity’s Last Exam, HLE (no tools)]
394 + family: humanitys-last-exam
395 + variant: full
396 + family_head: true
104 397 category: knowledge
105 398 task: expert-written frontier questions
106 399 metric: accuracy
400 + metric_label: accuracy
107 401 unit: "%"
402 + metric_min: 0
403 + metric_max: 100
404 + higher_is_better: true
405 + harness: Artificial Analysis and labs; text-only subset vs full multimodal set, with or without tools
406 + comparability_note: Tool use (search) and the text-only subset change scores substantially; check config before comparing.
108 407 website: https://lastexam.ai
109 408 paper: https://arxiv.org/abs/2501.14249
110 409 source_url: https://lastexam.ai/
111 410 - key: lmarena-text
112 411 name: LMArena text leaderboard
113 − aliases: [Chatbot Arena, LMSYS Chatbot Arena, Arena Elo]
412 + aliases: [Chatbot Arena, LMSYS Chatbot Arena, Arena Elo, LMArena, Arena Score]
413 + family: lmarena
414 + variant: text
415 + family_head: true
114 416 category: preference
115 417 task: crowdsourced pairwise human preference
116 − metric: Elo / Bradley–Terry score
418 + metric: elo
419 + metric_label: Elo / Bradley–Terry score
117 420 unit: ""
421 + metric_min: 0
422 + metric_max: null
423 + higher_is_better: true
424 + harness: LMArena live human votes (Bradley–Terry model)
425 + comparability_note: Relative score that drifts as the model pool changes; only a snapshot of the same date is comparable; style control changes rankings.
118 426 website: https://lmarena.ai
119 427 known_limitations: Style bias; sampling of prompts by users.
120 428 source_url: https://lmarena.ai/
121 429 - key: mmmu
122 430 name: MMMU
431 + aliases: [MMMU (val), Massive Multi-discipline Multimodal Understanding]
432 + family: mmmu
433 + variant: original
434 + family_head: true
123 435 category: multimodal
124 436 task: college-level multimodal understanding
125 437 metric: accuracy
438 + metric_label: accuracy
126 439 unit: "%"
440 + metric_min: 0
441 + metric_max: 100
442 + higher_is_better: true
443 + harness: self-reported by labs (validation split)
444 + comparability_note: Validation vs test split and chain-of-thought prompting differ across reporters.
127 445 website: https://mmmu-benchmark.github.io
128 446 paper: https://arxiv.org/abs/2311.16502
129 447 source_url: https://arxiv.org/abs/2311.16502
130 448 - key: tau-bench
131 449 name: τ-bench
132 − aliases: [tau-bench, TAU-bench]
450 + aliases: [tau-bench, TAU-bench, tau bench, τ-Bench]
451 + family: tau-bench
452 + variant: v1
453 + family_head: true
133 454 category: agentic
134 455 task: tool-agent-user interaction in retail/airline domains
135 456 metric: pass^1
457 + metric_label: pass^1
136 458 unit: "%"
459 + metric_min: 0
460 + metric_max: 100
461 + higher_is_better: true
462 + harness: simulated user (LLM) + tool environment; pass^k over k trials
463 + comparability_note: Domain (retail / airline), pass^k regime and the user-simulator model must match.
137 464 paper: https://arxiv.org/abs/2406.12045
138 465 source_url: https://arxiv.org/abs/2406.12045
139 466 - key: terminal-bench
140 467 name: Terminal-Bench
468 + aliases: [TerminalBench, Terminal Bench, Terminal-Bench 1.0]
469 + family: terminal-bench
470 + variant: "1.0"
471 + version: "1.0"
472 + family_head: true
141 473 category: agentic
142 474 task: terminal tasks solved by agents
143 475 metric: accuracy
476 + metric_label: accuracy
144 477 unit: "%"
478 + metric_min: 0
479 + metric_max: 100
480 + higher_is_better: true
481 + harness: Terminus agent (official) or the submitter's agent; Artificial Analysis reports its own harness versions (config.variant)
482 + comparability_note: Agent scaffold and harness version change results; config.variant (v2.1 / v4.0 / hard) must match.
483 + website: https://www.tbench.ai
484 + source_url: https://www.tbench.ai/
485 + - key: terminal-bench-2
486 + name: Terminal-Bench 2.0
487 + aliases: [Terminal Bench 2, TerminalBench 2.0, Terminal-Bench 2, TB2]
488 + family: terminal-bench
489 + variant: "2.0"
490 + version: "2.0"
491 + category: agentic
492 + task: revised, harder set of terminal tasks (Terminal-Bench 2.0)
493 + metric: accuracy
494 + metric_label: accuracy
495 + unit: "%"
496 + metric_min: 0
497 + metric_max: 100
498 + higher_is_better: true
499 + harness: Terminus 2 agent (official) or the submitter's agent
500 + comparability_note: Not comparable with Terminal-Bench 1.0; agent scaffold must match.
145 501 website: https://www.tbench.ai
146 502 source_url: https://www.tbench.ai/
147 503 - key: ifeval
148 504 name: IFEval
505 + aliases: [IF-Eval, Instruction-Following Eval, IFEval strict]
506 + family: ifeval
507 + variant: original
508 + family_head: true
149 509 category: instruction-following
150 510 task: verifiable instruction following
151 − metric: prompt-level strict accuracy
511 + metric: accuracy
512 + metric_label: prompt-level strict accuracy
152 513 unit: "%"
514 + metric_min: 0
515 + metric_max: 100
516 + higher_is_better: true
517 + harness: self-reported by labs
518 + comparability_note: Prompt-level vs instruction-level and strict vs loose accuracy are four different numbers.
153 519 paper: https://arxiv.org/abs/2311.07911
154 520 source_url: https://arxiv.org/abs/2311.07911
155 521 - key: mteb
156 522 name: MTEB
157 − aliases: [Massive Text Embedding Benchmark]
523 + aliases: [Massive Text Embedding Benchmark, MTEB (English), MTEB v1]
524 + family: mteb
525 + variant: v1
526 + family_head: true
158 527 category: embeddings
159 528 task: embedding tasks across retrieval, classification, clustering…
160 529 metric: mean score
530 + metric_label: mean score
161 531 unit: ""
532 + metric_min: 0
533 + metric_max: 100
534 + higher_is_better: true
535 + harness: MTEB library, official leaderboard
536 + comparability_note: Task set (English v1, v2, multilingual) and aggregation (mean of tasks vs mean of task types) must match.
162 537 website: https://huggingface.co/spaces/mteb/leaderboard
163 538 paper: https://arxiv.org/abs/2210.07316
164 539 source_url: https://arxiv.org/abs/2210.07316
540 + - key: mmteb
541 + name: MMTEB
542 + aliases: [Massive Multilingual Text Embedding Benchmark, MTEB multilingual, MTEB (Multilingual)]
543 + family: mteb
544 + variant: MMTEB
545 + category: embeddings
546 + task: multilingual extension of MTEB (500+ tasks across 250+ languages)
547 + metric: mean score
548 + metric_label: mean score
549 + unit: ""
550 + metric_min: 0
551 + metric_max: 100
552 + higher_is_better: true
553 + harness: MTEB library, official leaderboard
554 + comparability_note: Not comparable with English-only MTEB; the leaderboard's Borda rank and task-type mean are different aggregations.
555 + website: https://huggingface.co/spaces/mteb/leaderboard
556 + paper: https://arxiv.org/abs/2502.13595
557 + source_url: https://arxiv.org/abs/2502.13595
165 558 - key: artificial-analysis-intelligence-index
166 559 name: Artificial Analysis Intelligence Index
560 + aliases: [AA Intelligence Index, Intelligence Index, Artificial Analysis Index]
561 + family: artificial-analysis-intelligence-index
562 + variant: index
563 + family_head: true
167 564 category: composite
168 565 task: composite of several evaluations run by Artificial Analysis
169 566 metric: index
567 + metric_label: index
170 568 unit: ""
569 + metric_min: 0
570 + metric_max: 100
571 + higher_is_better: true
572 + harness: Artificial Analysis internal runs; config.version = index methodology version
573 + comparability_note: Methodology versions change the component set — compare within one version (run group) only; reasoning-effort variants are configurations.
171 574 website: https://artificialanalysis.ai
172 575 known_limitations: Proprietary composite; methodology versions change.
173 576 source_url: https://artificialanalysis.ai/methodology
modified registry/organizations.d/ecosystem.yaml +29 −1
@@ -1,9 +1,10 @@
1 1 # Ecosystem organizations referenced by the code/hub connectors (GitHub seed list, PyPI). Fragment of registry/organizations.yaml.
2 −# Only identity fields we can read from the organization's own pages; unknown fields are omitted.
2 +# Only identity fields we can read from the organization's own pages; unknown fields are omitted. `kind` = canonical org kind (see organizations.yaml).
3 3 organizations:
4 4 - key: eleutherai
5 5 name: EleutherAI
6 6 type: lab
7 + kind: nonprofit
7 8 aliases: [Eleuther AI]
8 9 domains: [eleuther.ai]
9 10 hf_org: EleutherAI
@@ -13,6 +14,7 @@ organizations:
13 14 - key: stanford-crfm
14 15 name: Stanford CRFM
15 16 type: university
17 + kind: university
16 18 aliases: [Center for Research on Foundation Models, Stanford Center for Research on Foundation Models]
17 19 domains: [crfm.stanford.edu]
18 20 hf_org: stanford-crfm
@@ -23,6 +25,7 @@ organizations:
23 25 - key: berriai
24 26 name: BerriAI
25 27 type: company
28 + kind: company
26 29 aliases: [LiteLLM, Berri AI]
27 30 domains: [litellm.ai, docs.litellm.ai]
28 31 github_org: BerriAI
@@ -31,6 +34,7 @@ organizations:
31 34 - key: chroma
32 35 name: Chroma
33 36 type: company
37 + kind: company
34 38 aliases: [Chroma DB, ChromaDB, trychroma]
35 39 domains: [trychroma.com]
36 40 github_org: chroma-core
@@ -39,6 +43,7 @@ organizations:
39 43 - key: qdrant
40 44 name: Qdrant
41 45 type: company
46 + kind: company
42 47 domains: [qdrant.tech]
43 48 github_org: qdrant
44 49 website: https://qdrant.tech
@@ -46,6 +51,7 @@ organizations:
46 51 - key: zilliz
47 52 name: Zilliz
48 53 type: company
54 + kind: company
49 55 aliases: [Milvus]
50 56 domains: [zilliz.com, milvus.io]
51 57 github_org: milvus-io
@@ -54,6 +60,7 @@ organizations:
54 60 - key: weaviate
55 61 name: Weaviate
56 62 type: company
63 + kind: company
57 64 domains: [weaviate.io]
58 65 github_org: weaviate
59 66 website: https://weaviate.io
@@ -61,6 +68,7 @@ organizations:
61 68 - key: lancedb
62 69 name: LanceDB
63 70 type: company
71 + kind: company
64 72 domains: [lancedb.com]
65 73 github_org: lancedb
66 74 website: https://lancedb.com
@@ -68,6 +76,7 @@ organizations:
68 76 - key: all-hands-ai
69 77 name: All Hands AI
70 78 type: company
79 + kind: company
71 80 aliases: [OpenHands, All-Hands-AI]
72 81 domains: [all-hands.dev]
73 82 github_org: OpenHands
@@ -76,6 +85,7 @@ organizations:
76 85 - key: crewai
77 86 name: CrewAI
78 87 type: company
88 + kind: company
79 89 aliases: [crewAI Inc.]
80 90 domains: [crewai.com]
81 91 github_org: crewAIInc
@@ -84,6 +94,7 @@ organizations:
84 94 - key: pydantic
85 95 name: Pydantic
86 96 type: company
97 + kind: company
87 98 aliases: [Pydantic Services]
88 99 domains: [pydantic.dev]
89 100 github_org: pydantic
@@ -92,6 +103,7 @@ organizations:
92 103 - key: browser-use
93 104 name: Browser Use
94 105 type: company
106 + kind: company
95 107 aliases: [browser-use]
96 108 domains: [browser-use.com]
97 109 github_org: browser-use
@@ -100,12 +112,14 @@ organizations:
100 112 - key: exo-labs
101 113 name: EXO Labs
102 114 type: company
115 + kind: company
103 116 aliases: [exo, exo-explore]
104 117 github_org: exo-explore
105 118 source_url: https://github.com/exo-explore
106 119 - key: lm-studio
107 120 name: LM Studio
108 121 type: company
122 + kind: company
109 123 aliases: [Element Labs, lmstudio]
110 124 domains: [lmstudio.ai]
111 125 github_org: lmstudio-ai
@@ -114,6 +128,7 @@ organizations:
114 128 - key: comfy-org
115 129 name: Comfy Org
116 130 type: organization
131 + kind: community
117 132 aliases: [ComfyUI, comfyanonymous]
118 133 domains: [comfy.org]
119 134 github_org: Comfy-Org
@@ -130,6 +145,7 @@ organizations:
130 145 - key: internlm
131 146 name: InternLM (Shanghai AI Laboratory)
132 147 type: lab
148 + kind: lab
133 149 aliases: [InternLM, Shanghai AI Lab, Shanghai Artificial Intelligence Laboratory]
134 150 hf_org: internlm
135 151 github_org: InternLM
@@ -138,12 +154,14 @@ organizations:
138 154 - key: axolotl-ai
139 155 name: Axolotl AI
140 156 type: organization
157 + kind: community
141 158 aliases: [axolotl]
142 159 github_org: axolotl-ai-cloud
143 160 source_url: https://github.com/axolotl-ai-cloud
144 161 - key: vibrant-labs
145 162 name: Vibrant Labs (Ragas)
146 163 type: company
164 + kind: company
147 165 aliases: [Ragas, explodinggradients, Exploding Gradients, vibrantlabsai]
148 166 domains: [ragas.io]
149 167 github_org: vibrantlabsai
@@ -152,6 +170,7 @@ organizations:
152 170 - key: langfuse
153 171 name: Langfuse
154 172 type: company
173 + kind: company
155 174 domains: [langfuse.com]
156 175 github_org: langfuse
157 176 website: https://langfuse.com
@@ -159,6 +178,7 @@ organizations:
159 178 - key: arize
160 179 name: Arize AI
161 180 type: company
181 + kind: company
162 182 aliases: [Arize, Arize Phoenix]
163 183 domains: [arize.com]
164 184 github_org: Arize-ai
@@ -167,6 +187,7 @@ organizations:
167 187 - key: anyscale
168 188 name: Anyscale
169 189 type: company
190 + kind: company
170 191 aliases: [Ray, Ray project]
171 192 domains: [anyscale.com, ray.io]
172 193 github_org: ray-project
@@ -175,6 +196,7 @@ organizations:
175 196 - key: kubeflow
176 197 name: Kubeflow
177 198 type: organization
199 + kind: community
178 200 domains: [kubeflow.org]
179 201 github_org: kubeflow
180 202 website: https://www.kubeflow.org
@@ -182,6 +204,7 @@ organizations:
182 204 - key: bentoml
183 205 name: BentoML
184 206 type: company
207 + kind: company
185 208 domains: [bentoml.com]
186 209 github_org: bentoml
187 210 website: https://www.bentoml.com
@@ -189,6 +212,7 @@ organizations:
189 212 - key: lightning-ai
190 213 name: Lightning AI
191 214 type: company
215 + kind: company
192 216 aliases: [PyTorch Lightning, Grid.ai]
193 217 domains: [lightning.ai]
194 218 github_org: Lightning-AI
@@ -197,18 +221,21 @@ organizations:
197 221 - key: dao-ailab
198 222 name: Dao AI Lab
199 223 type: lab
224 + kind: lab
200 225 aliases: [Dao-AILab, FlashAttention]
201 226 github_org: Dao-AILab
202 227 source_url: https://github.com/Dao-AILab
203 228 - key: turboderp
204 229 name: turboderp
205 230 type: organization
231 + kind: individual
206 232 aliases: [ExLlama, turboderp-org]
207 233 github_org: turboderp-org
208 234 source_url: https://github.com/turboderp-org
209 235 - key: model-context-protocol
210 236 name: Model Context Protocol
211 237 type: organization
238 + kind: community
212 239 aliases: [MCP, modelcontextprotocol]
213 240 domains: [modelcontextprotocol.io]
214 241 github_org: modelcontextprotocol
@@ -217,6 +244,7 @@ organizations:
217 244 - key: canopy-labs
218 245 name: Canopy Labs
219 246 type: company
247 + kind: company
220 248 aliases: [canopylabs]
221 249 hf_org: canopylabs
222 250 source_url: https://huggingface.co/canopylabs
modified registry/organizations.yaml +169 −9
@@ -1,11 +1,17 @@
1 1 # Canonical organizations (companies, labs, universities, foundations). Deterministic aliases and identifiers feed entity resolution;
2 2 # every factual attribute carries the page it was read from (`source_url`). Fields not known are omitted — never guessed.
3 +#
4 +# `type` = entity type (company | lab | organization | university) — kept for slugs and back-compat.
5 +# `kind` = canonical organisation kind (aiatlas.ontology.taxonomy.ORG_KINDS: company | lab | university | nonprofit | government | community |
6 +# consortium | individual) → attribute `org_kind`. Omitted when we are not sure; the seed then falls back to the type's default (none for `organization`).
7 +# `country` = ISO-2 of the legal seat as stated on the organisation's own site; omitted when not stated there.
3 8 organizations:
4 9 - key: openai
5 10 name: OpenAI
6 11 type: company
12 + kind: company
7 13 aliases: [OpenAI Inc., OpenAI, L.P., OpenAI OpCo]
8 − domains: [openai.com, platform.openai.com, chatgpt.com]
14 + domains: [openai.com, platform.openai.com, chatgpt.com, developers.openai.com]
9 15 hf_org: openai
10 16 github_org: openai
11 17 country: US
@@ -16,8 +22,9 @@ organizations:
16 22 - key: anthropic
17 23 name: Anthropic
18 24 type: company
25 + kind: company
19 26 aliases: [Anthropic PBC]
20 − domains: [anthropic.com, claude.com, docs.claude.com, claude.ai]
27 + domains: [anthropic.com, claude.com, docs.claude.com, claude.ai, platform.claude.com]
21 28 hf_org: Anthropic
22 29 github_org: anthropics
23 30 country: US
@@ -28,6 +35,7 @@ organizations:
28 35 - key: google
29 36 name: Google
30 37 type: company
38 + kind: company
31 39 aliases: [Google LLC, Alphabet, Google AI, Google Research]
32 40 domains: [google.com, ai.google, ai.google.dev, research.google, blog.google, cloud.google.com]
33 41 hf_org: google
@@ -39,6 +47,7 @@ organizations:
39 47 - key: google-deepmind
40 48 name: Google DeepMind
41 49 type: lab
50 + kind: lab
42 51 aliases: [DeepMind, DeepMind Technologies, Google Brain]
43 52 domains: [deepmind.google, deepmind.com]
44 53 github_org: google-deepmind
@@ -51,6 +60,7 @@ organizations:
51 60 - key: meta
52 61 name: Meta Platforms
53 62 type: company
63 + kind: company
54 64 aliases: [Meta, Facebook, Facebook Inc.]
55 65 domains: [meta.com, about.fb.com]
56 66 country: US
@@ -60,7 +70,8 @@ organizations:
60 70 - key: meta-ai
61 71 name: Meta AI
62 72 type: lab
63 − aliases: [FAIR, Facebook AI Research, Meta FAIR, Meta AI Research]
73 + kind: lab
74 + aliases: [FAIR, Facebook AI Research, Meta FAIR, Meta AI Research, Meta Superintelligence Labs]
64 75 domains: [ai.meta.com, llama.com, ai.facebook.com]
65 76 hf_org: meta-llama
66 77 github_org: facebookresearch
@@ -71,6 +82,7 @@ organizations:
71 82 - key: microsoft
72 83 name: Microsoft
73 84 type: company
85 + kind: company
74 86 aliases: [Microsoft Corporation, Microsoft Research, Microsoft AI, Azure AI]
75 87 domains: [microsoft.com, azure.microsoft.com, learn.microsoft.com]
76 88 hf_org: microsoft
@@ -83,6 +95,7 @@ organizations:
83 95 - key: nvidia
84 96 name: NVIDIA
85 97 type: company
98 + kind: company
86 99 aliases: [NVIDIA Corporation, Nvidia, NVIDIA AI, NVIDIA Research]
87 100 domains: [nvidia.com, developer.nvidia.com, blogs.nvidia.com, build.nvidia.com]
88 101 hf_org: nvidia
@@ -95,6 +108,7 @@ organizations:
95 108 - key: apple
96 109 name: Apple
97 110 type: company
111 + kind: company
98 112 aliases: [Apple Inc., Apple Machine Learning Research]
99 113 domains: [apple.com, machinelearning.apple.com]
100 114 hf_org: apple
@@ -107,6 +121,7 @@ organizations:
107 121 - key: mistral
108 122 name: Mistral AI
109 123 type: company
124 + kind: company
110 125 aliases: [Mistral, MistralAI]
111 126 domains: [mistral.ai, docs.mistral.ai, console.mistral.ai]
112 127 hf_org: mistralai
@@ -119,6 +134,7 @@ organizations:
119 134 - key: alibaba
120 135 name: Alibaba Group
121 136 type: company
137 + kind: company
122 138 aliases: [Alibaba, Alibaba Cloud, Aliyun]
123 139 domains: [alibabacloud.com, alibaba.com]
124 140 country: CN
@@ -128,6 +144,7 @@ organizations:
128 144 - key: qwen
129 145 name: Qwen
130 146 type: lab
147 + kind: lab
131 148 aliases: [Qwen Team, Alibaba Qwen, Tongyi Qianwen, QwenLM]
132 149 domains: [qwenlm.github.io, qwen.ai, chat.qwen.ai]
133 150 hf_org: Qwen
@@ -139,6 +156,7 @@ organizations:
139 156 - key: deepseek
140 157 name: DeepSeek
141 158 type: company
159 + kind: company
142 160 aliases: [DeepSeek AI, DeepSeek-AI, Hangzhou DeepSeek Artificial Intelligence]
143 161 domains: [deepseek.com, api-docs.deepseek.com, chat.deepseek.com]
144 162 hf_org: deepseek-ai
@@ -151,7 +169,8 @@ organizations:
151 169 - key: cohere
152 170 name: Cohere
153 171 type: company
154 − aliases: [Cohere Inc., Cohere For AI, C4AI]
172 + kind: company
173 + aliases: [Cohere Inc., Cohere For AI, C4AI, Cohere Labs]
155 174 domains: [cohere.com, docs.cohere.com, cohere.ai]
156 175 hf_org: CohereLabs
157 176 github_org: cohere-ai
@@ -163,6 +182,7 @@ organizations:
163 182 - key: xai
164 183 name: xAI
165 184 type: company
185 + kind: company
166 186 aliases: [x.ai, xAI Corp]
167 187 domains: [x.ai, docs.x.ai]
168 188 hf_org: xai-org
@@ -174,6 +194,7 @@ organizations:
174 194 - key: huggingface
175 195 name: Hugging Face
176 196 type: company
197 + kind: company
177 198 aliases: [HuggingFace, HF]
178 199 domains: [huggingface.co, hf.co]
179 200 hf_org: HuggingFaceTB
@@ -186,6 +207,7 @@ organizations:
186 207 - key: stability-ai
187 208 name: Stability AI
188 209 type: company
210 + kind: company
189 211 aliases: [StabilityAI]
190 212 domains: [stability.ai]
191 213 hf_org: stabilityai
@@ -196,6 +218,7 @@ organizations:
196 218 - key: ai21
197 219 name: AI21 Labs
198 220 type: company
221 + kind: company
199 222 aliases: [AI21]
200 223 domains: [ai21.com]
201 224 hf_org: ai21labs
@@ -207,6 +230,7 @@ organizations:
207 230 - key: together-ai
208 231 name: Together AI
209 232 type: company
233 + kind: company
210 234 aliases: [Together, Together Computer]
211 235 domains: [together.ai, together.xyz]
212 236 hf_org: togethercomputer
@@ -217,8 +241,9 @@ organizations:
217 241 - key: fireworks-ai
218 242 name: Fireworks AI
219 243 type: company
244 + kind: company
220 245 aliases: [Fireworks]
221 − domains: [fireworks.ai]
246 + domains: [fireworks.ai, docs.fireworks.ai]
222 247 hf_org: fireworks-ai
223 248 github_org: fw-ai
224 249 country: US
@@ -227,6 +252,7 @@ organizations:
227 252 - key: groq
228 253 name: Groq
229 254 type: company
255 + kind: company
230 256 aliases: [Groq Inc., GroqCloud]
231 257 domains: [groq.com, console.groq.com]
232 258 github_org: groq
@@ -237,6 +263,7 @@ organizations:
237 263 - key: cerebras
238 264 name: Cerebras Systems
239 265 type: company
266 + kind: company
240 267 aliases: [Cerebras]
241 268 domains: [cerebras.ai, cerebras.net]
242 269 hf_org: cerebras
@@ -247,6 +274,7 @@ organizations:
247 274 - key: sambanova
248 275 name: SambaNova Systems
249 276 type: company
277 + kind: company
250 278 aliases: [SambaNova]
251 279 domains: [sambanova.ai]
252 280 country: US
@@ -255,6 +283,7 @@ organizations:
255 283 - key: perplexity
256 284 name: Perplexity AI
257 285 type: company
286 + kind: company
258 287 aliases: [Perplexity]
259 288 domains: [perplexity.ai]
260 289 hf_org: perplexity-ai
@@ -264,6 +293,7 @@ organizations:
264 293 - key: openrouter
265 294 name: OpenRouter
266 295 type: company
296 + kind: company
267 297 aliases: [OpenRouter.ai]
268 298 domains: [openrouter.ai]
269 299 country: US
@@ -272,12 +302,14 @@ organizations:
272 302 - key: deepinfra
273 303 name: DeepInfra
274 304 type: company
305 + kind: company
275 306 domains: [deepinfra.com]
276 307 website: https://deepinfra.com
277 308 source_url: https://deepinfra.com/about
278 309 - key: replicate
279 310 name: Replicate
280 311 type: company
312 + kind: company
281 313 domains: [replicate.com]
282 314 github_org: replicate
283 315 country: US
@@ -286,6 +318,7 @@ organizations:
286 318 - key: cloudflare
287 319 name: Cloudflare
288 320 type: company
321 + kind: company
289 322 aliases: [Cloudflare Workers AI]
290 323 domains: [cloudflare.com, developers.cloudflare.com]
291 324 country: US
@@ -294,7 +327,8 @@ organizations:
294 327 - key: amazon
295 328 name: Amazon Web Services
296 329 type: company
297 − aliases: [AWS, Amazon Bedrock, Amazon, Amazon.com]
330 + kind: company
331 + aliases: [AWS, Amazon Bedrock, Amazon, Amazon.com, Amazon AGI]
298 332 domains: [aws.amazon.com, amazon.com]
299 333 hf_org: amazon
300 334 github_org: aws
@@ -304,6 +338,7 @@ organizations:
304 338 - key: elevenlabs
305 339 name: ElevenLabs
306 340 type: company
341 + kind: company
307 342 domains: [elevenlabs.io]
308 343 country: US
309 344 website: https://elevenlabs.io
@@ -311,6 +346,7 @@ organizations:
311 346 - key: runway
312 347 name: Runway
313 348 type: company
349 + kind: company
314 350 aliases: [Runway ML, RunwayML]
315 351 domains: [runwayml.com]
316 352 country: US
@@ -319,6 +355,7 @@ organizations:
319 355 - key: amd
320 356 name: AMD
321 357 type: company
358 + kind: company
322 359 aliases: [Advanced Micro Devices]
323 360 domains: [amd.com]
324 361 hf_org: amd
@@ -330,6 +367,7 @@ organizations:
330 367 - key: intel
331 368 name: Intel
332 369 type: company
370 + kind: company
333 371 aliases: [Intel Corporation]
334 372 domains: [intel.com]
335 373 hf_org: Intel
@@ -340,6 +378,7 @@ organizations:
340 378 - key: arxiv
341 379 name: arXiv
342 380 type: organization
381 + kind: nonprofit
343 382 aliases: [arXiv.org, Cornell arXiv]
344 383 domains: [arxiv.org, export.arxiv.org, rss.arxiv.org]
345 384 website: https://arxiv.org
@@ -347,12 +386,14 @@ organizations:
347 386 - key: openreview
348 387 name: OpenReview
349 388 type: organization
389 + kind: nonprofit
350 390 domains: [openreview.net]
351 391 website: https://openreview.net
352 392 source_url: https://openreview.net/about
353 393 - key: github
354 394 name: GitHub
355 395 type: company
396 + kind: company
356 397 aliases: [GitHub Inc.]
357 398 domains: [github.com, raw.githubusercontent.com]
358 399 parent: microsoft
@@ -362,13 +403,16 @@ organizations:
362 403 - key: python-software-foundation
363 404 name: Python Software Foundation
364 405 type: organization
406 + kind: nonprofit
365 407 aliases: [PSF, PyPI]
366 408 domains: [pypi.org, python.org]
409 + country: US
367 410 website: https://www.python.org/psf/
368 411 source_url: https://www.python.org/psf/about/
369 412 - key: aider
370 413 name: Aider
371 414 type: organization
415 + kind: community
372 416 aliases: [Aider AI]
373 417 domains: [aider.chat]
374 418 github_org: Aider-AI
@@ -377,6 +421,7 @@ organizations:
377 421 - key: swe-bench
378 422 name: SWE-bench
379 423 type: organization
424 + kind: community
380 425 aliases: [SWE-bench team, Princeton SWE-bench]
381 426 domains: [swebench.com]
382 427 github_org: SWE-bench
@@ -385,6 +430,7 @@ organizations:
385 430 - key: livebench
386 431 name: LiveBench
387 432 type: organization
433 + kind: community
388 434 domains: [livebench.ai]
389 435 github_org: LiveBench
390 436 website: https://livebench.ai
@@ -392,6 +438,7 @@ organizations:
392 438 - key: artificial-analysis
393 439 name: Artificial Analysis
394 440 type: company
441 + kind: company
395 442 domains: [artificialanalysis.ai]
396 443 website: https://artificialanalysis.ai
397 444 source_url: https://artificialanalysis.ai/
@@ -404,6 +451,7 @@ organizations:
404 451 - key: pytorch-foundation
405 452 name: PyTorch Foundation
406 453 type: organization
454 + kind: nonprofit
407 455 aliases: [PyTorch, Linux Foundation PyTorch]
408 456 domains: [pytorch.org]
409 457 github_org: pytorch
@@ -412,6 +460,7 @@ organizations:
412 460 - key: ggml
413 461 name: ggml.ai
414 462 type: organization
463 + kind: company
415 464 aliases: [ggml, ggml-org, llama.cpp project]
416 465 domains: [ggml.ai]
417 466 github_org: ggml-org
@@ -420,6 +469,7 @@ organizations:
420 469 - key: vllm
421 470 name: vLLM project
422 471 type: organization
472 + kind: community
423 473 aliases: [vLLM]
424 474 domains: [vllm.ai, docs.vllm.ai]
425 475 github_org: vllm-project
@@ -428,6 +478,7 @@ organizations:
428 478 - key: ollama
429 479 name: Ollama
430 480 type: company
481 + kind: company
431 482 domains: [ollama.com]
432 483 github_org: ollama
433 484 website: https://ollama.com
@@ -435,6 +486,7 @@ organizations:
435 486 - key: langchain
436 487 name: LangChain
437 488 type: company
489 + kind: company
438 490 aliases: [LangChain Inc., LangGraph]
439 491 domains: [langchain.com]
440 492 github_org: langchain-ai
@@ -444,6 +496,7 @@ organizations:
444 496 - key: llamaindex
445 497 name: LlamaIndex
446 498 type: company
499 + kind: company
447 500 aliases: [Llama Index, run-llama]
448 501 domains: [llamaindex.ai]
449 502 github_org: run-llama
@@ -452,6 +505,7 @@ organizations:
452 505 - key: sglang
453 506 name: SGLang project
454 507 type: organization
508 + kind: community
455 509 aliases: [SGLang, LMSYS]
456 510 domains: [sglang.ai, lmsys.org]
457 511 github_org: sgl-project
@@ -460,6 +514,8 @@ organizations:
460 514 - key: unsloth
461 515 name: Unsloth
462 516 type: company
517 + kind: company
518 + aliases: [Unsloth AI]
463 519 domains: [unsloth.ai]
464 520 hf_org: unsloth
465 521 github_org: unslothai
@@ -468,6 +524,7 @@ organizations:
468 524 - key: bartowski
469 525 name: bartowski
470 526 type: organization
527 + kind: individual
471 528 aliases: [Bartowski quantizations]
472 529 hf_org: bartowski
473 530 website: https://huggingface.co/bartowski
@@ -475,6 +532,7 @@ organizations:
475 532 - key: mlx-community
476 533 name: MLX Community
477 534 type: organization
535 + kind: community
478 536 aliases: [mlx-community]
479 537 hf_org: mlx-community
480 538 github_org: ml-explore
@@ -483,6 +541,7 @@ organizations:
483 541 - key: allenai
484 542 name: Allen Institute for AI
485 543 type: lab
544 + kind: nonprofit
486 545 aliases: [AI2, AllenAI, Ai2]
487 546 domains: [allenai.org]
488 547 hf_org: allenai
@@ -494,6 +553,8 @@ organizations:
494 553 - key: nous-research
495 554 name: Nous Research
496 555 type: company
556 + kind: company
557 + aliases: [Nous, NousResearch]
497 558 domains: [nousresearch.com]
498 559 hf_org: NousResearch
499 560 github_org: NousResearch
@@ -502,7 +563,8 @@ organizations:
502 563 - key: zhipu
503 564 name: Z.ai (Zhipu AI)
504 565 type: company
505 − aliases: [Zhipu AI, Zhipu, THUDM, Z.ai, ChatGLM]
566 + kind: company
567 + aliases: [Zhipu AI, Zhipu, THUDM, Z.ai, ChatGLM, Z AI]
506 568 domains: [z.ai, zhipuai.cn, bigmodel.cn]
507 569 hf_org: zai-org
508 570 github_org: zai-org
@@ -513,7 +575,8 @@ organizations:
513 575 - key: moonshot
514 576 name: Moonshot AI
515 577 type: company
516 − aliases: [Moonshot, Kimi]
578 + kind: company
579 + aliases: [Moonshot, Kimi, MoonshotAI]
517 580 domains: [moonshot.cn, moonshot.ai, kimi.com]
518 581 hf_org: moonshotai
519 582 github_org: MoonshotAI
@@ -524,7 +587,8 @@ organizations:
524 587 - key: minimax
525 588 name: MiniMax
526 589 type: company
527 − aliases: [MiniMax AI]
590 + kind: company
591 + aliases: [MiniMax AI, MiniMaxAI]
528 592 domains: [minimax.io, minimaxi.com]
529 593 hf_org: MiniMaxAI
530 594 github_org: MiniMax-AI
@@ -534,6 +598,7 @@ organizations:
534 598 - key: bytedance
535 599 name: ByteDance
536 600 type: company
601 + kind: company
537 602 aliases: [ByteDance Seed, Seed, Doubao]
538 603 domains: [bytedance.com, seed.bytedance.com]
539 604 hf_org: ByteDance-Seed
@@ -544,6 +609,7 @@ organizations:
544 609 - key: baidu
545 610 name: Baidu
546 611 type: company
612 + kind: company
547 613 aliases: [Baidu ERNIE, ERNIE]
548 614 domains: [baidu.com, ernie.baidu.com]
549 615 hf_org: baidu
@@ -554,6 +620,7 @@ organizations:
554 620 - key: tencent
555 621 name: Tencent
556 622 type: company
623 + kind: company
557 624 aliases: [Tencent Hunyuan, Hunyuan]
558 625 domains: [tencent.com, hunyuan.tencent.com]
559 626 hf_org: tencent
@@ -564,6 +631,7 @@ organizations:
564 631 - key: ibm
565 632 name: IBM
566 633 type: company
634 + kind: company
567 635 aliases: [IBM Research, IBM Granite]
568 636 domains: [ibm.com, research.ibm.com]
569 637 hf_org: ibm-granite
@@ -574,6 +642,7 @@ organizations:
574 642 - key: databricks
575 643 name: Databricks
576 644 type: company
645 + kind: company
577 646 aliases: [Mosaic, MosaicML]
578 647 domains: [databricks.com]
579 648 hf_org: databricks
@@ -584,6 +653,7 @@ organizations:
584 653 - key: salesforce
585 654 name: Salesforce
586 655 type: company
656 + kind: company
587 657 aliases: [Salesforce AI Research]
588 658 domains: [salesforce.com]
589 659 hf_org: Salesforce
@@ -594,6 +664,7 @@ organizations:
594 664 - key: black-forest-labs
595 665 name: Black Forest Labs
596 666 type: company
667 + kind: company
597 668 aliases: [BFL, FLUX]
598 669 domains: [blackforestlabs.ai, bfl.ai]
599 670 hf_org: black-forest-labs
@@ -604,6 +675,8 @@ organizations:
604 675 - key: liquid-ai
605 676 name: Liquid AI
606 677 type: company
678 + kind: company
679 + aliases: [Liquid]
607 680 domains: [liquid.ai]
608 681 hf_org: LiquidAI
609 682 country: US
@@ -612,8 +685,95 @@ organizations:
612 685 - key: ml-explore
613 686 name: Apple ML Explore (MLX)
614 687 type: lab
688 + kind: lab
615 689 aliases: [MLX, ml-explore]
616 690 github_org: ml-explore
617 691 parent: apple
692 + country: US
618 693 website: https://ml-explore.github.io/mlx
619 694 source_url: https://github.com/ml-explore/mlx
695 + # ---------------------------------------------------------------- labs that appear on leaderboards (Artificial Analysis / LiveBench creators)
696 + - key: upstage
697 + name: Upstage
698 + type: company
699 + kind: company
700 + aliases: [Upstage AI, upstage]
701 + domains: [upstage.ai, console.upstage.ai]
702 + hf_org: upstage
703 + github_org: UpstageAI
704 + country: KR
705 + website: https://www.upstage.ai
706 + source_url: https://www.upstage.ai/
707 + - key: stepfun
708 + name: StepFun
709 + type: company
710 + kind: company
711 + aliases: [Step Fun, stepfun-ai, 阶跃星辰, StepFun AI]
712 + domains: [stepfun.com, platform.stepfun.com]
713 + hf_org: stepfun-ai
714 + github_org: stepfun-ai
715 + country: CN
716 + website: https://www.stepfun.com
717 + source_url: https://www.stepfun.com/
718 + - key: kwaipilot
719 + name: Kwaipilot
720 + type: company # entity type of the existing production row (hf_org Kwaipilot); the canonical kind is `lab`
721 + kind: lab
722 + aliases: [KwaiPilot, Kuaishou Kwaipilot, KAT]
723 + hf_org: Kwaipilot
724 + github_org: Kwaipilot
725 + parent: kuaishou
726 + country: CN
727 + website: https://github.com/Kwaipilot
728 + source_url: https://github.com/Kwaipilot
729 + - key: kuaishou
730 + name: Kuaishou
731 + type: company
732 + kind: company
733 + aliases: [Kuaishou Technology, Kling AI, Kwai]
734 + domains: [kuaishou.com, klingai.com]
735 + country: CN
736 + website: https://www.kuaishou.com
737 + source_url: https://ir.kuaishou.com/
738 + - key: thinking-machines
739 + name: Thinking Machines
740 + type: company
741 + kind: company
742 + aliases: [Thinking Machines Lab, thinkingmachines, Thinking Machines Lab Inc.]
743 + domains: [thinkingmachines.ai]
744 + github_org: thinking-machines-lab # no hf_org: the hub organisation is not stated on thinkingmachines.ai (a wrong value would split the entity)
745 + country: US
746 + website: https://thinkingmachines.ai
747 + source_url: https://thinkingmachines.ai/
748 + - key: lg-ai-research
749 + name: LG AI Research
750 + type: lab
751 + kind: lab
752 + aliases: [LGAI, LG AI, EXAONE, LGAI-EXAONE]
753 + domains: [lgresearch.ai]
754 + hf_org: LGAI-EXAONE
755 + github_org: LG-AI-EXAONE
756 + country: KR
757 + website: https://www.lgresearch.ai
758 + source_url: https://www.lgresearch.ai/
759 + - key: xiaomi
760 + name: Xiaomi
761 + type: company
762 + kind: company
763 + aliases: [Xiaomi MiMo, MiMo, Xiaomi Corporation]
764 + domains: [mi.com, xiaomimimo.com]
765 + hf_org: XiaomiMiMo
766 + github_org: XiaomiMiMo
767 + country: CN
768 + website: https://www.mi.com
769 + source_url: https://www.mi.com/global/about/
770 + - key: inclusionai
771 + name: inclusionAI (Ant Group)
772 + type: company # entity type of the existing production row (AA creator / OpenRouter vendor); the canonical kind is `lab`
773 + kind: lab
774 + aliases: [inclusionAI, InclusionAI, Ant Group AGI, Ling]
775 + hf_org: inclusionAI
776 + github_org: inclusionAI
777 + country: CN
778 + website: https://github.com/inclusionAI
779 + source_url: https://github.com/inclusionAI
modified registry/repositories.yaml +61 −58
@@ -1,79 +1,82 @@
1 1 # Seed list of open-source AI repositories followed by the `github` and `pypi` connectors (loaded with `aiatlas.registry.load("repositories")`).
2 2 # `repo` is the canonical owner/name on GitHub (verified live on 2026-09-11; renamed repositories point to their redirect target),
3 −# `kind` ∈ framework|runtime|library|agent|tool|model, `organization` is a registry/organizations key when the owner is known,
4 −# `pypi` is the distribution name when the project ships a Python package under the same identity.
3 +# `kind` is a canonical framework kind from aiatlas.ontology.taxonomy.FRAMEWORK_KINDS (training-framework | inference-engine | serving-engine |
4 +# library | runtime | agent-framework | orchestration | evaluation-harness | sdk | tool | application | agent | mcp-server | vector-database |
5 +# observability | data-tooling) or `model` for official model-code releases. The entity type follows the kind: agent → `agent`,
6 +# tool | application | mcp-server → `tool`, model → `repository`, everything else → `framework`. Agents carry `agent_kind` (coding | browser | research).
7 +# `organization` is a registry/organizations key when the owner is known, `pypi` the distribution name when the project ships a Python package.
5 8 repositories:
6 9 # ---------------------------------------------------------------- frameworks & training
7 − - {key: pytorch, repo: pytorch/pytorch, kind: framework, organization: pytorch-foundation, pypi: torch}
10 + - {key: pytorch, repo: pytorch/pytorch, kind: training-framework, organization: pytorch-foundation, pypi: torch}
8 11 - {key: transformers, repo: huggingface/transformers, kind: library, organization: huggingface, pypi: transformers}
9 − - {key: jax, repo: jax-ml/jax, kind: framework, organization: google, pypi: jax}
12 + - {key: jax, repo: jax-ml/jax, kind: training-framework, organization: google, pypi: jax}
10 13 - {key: diffusers, repo: huggingface/diffusers, kind: library, organization: huggingface, pypi: diffusers}
11 14 - {key: peft, repo: huggingface/peft, kind: library, organization: huggingface, pypi: peft}
12 15 - {key: trl, repo: huggingface/trl, kind: library, organization: huggingface, pypi: trl}
13 − - {key: unsloth, repo: unslothai/unsloth, kind: library, organization: unsloth, pypi: unsloth}
14 − - {key: axolotl, repo: axolotl-ai-cloud/axolotl, kind: tool, organization: axolotl-ai, pypi: axolotl}
15 − - {key: pytorch-lightning, repo: Lightning-AI/pytorch-lightning, kind: framework, organization: lightning-ai, pypi: lightning}
16 − - {key: ray, repo: ray-project/ray, kind: framework, organization: anyscale, pypi: ray}
17 − - {key: kubeflow, repo: kubeflow/kubeflow, kind: tool, organization: kubeflow}
18 − - {key: bentoml, repo: bentoml/BentoML, kind: framework, organization: bentoml, pypi: bentoml}
16 + - {key: unsloth, repo: unslothai/unsloth, kind: training-framework, organization: unsloth, pypi: unsloth}
17 + - {key: axolotl, repo: axolotl-ai-cloud/axolotl, kind: training-framework, organization: axolotl-ai, pypi: axolotl}
18 + - {key: pytorch-lightning, repo: Lightning-AI/pytorch-lightning, kind: training-framework, organization: lightning-ai, pypi: lightning}
19 + - {key: ray, repo: ray-project/ray, kind: orchestration, organization: anyscale, pypi: ray}
20 + - {key: kubeflow, repo: kubeflow/kubeflow, kind: orchestration, organization: kubeflow}
21 + - {key: bentoml, repo: bentoml/BentoML, kind: serving-engine, organization: bentoml, pypi: bentoml}
19 22 - {key: triton, repo: triton-lang/triton, kind: library, organization: openai, pypi: triton}
20 23 - {key: flash-attention, repo: Dao-AILab/flash-attention, kind: library, organization: dao-ailab, pypi: flash-attn}
21 − - {key: nemo, repo: NVIDIA-NeMo/Speech, kind: framework, organization: nvidia, pypi: nemo-toolkit}
24 + - {key: nemo, repo: NVIDIA-NeMo/Speech, kind: training-framework, organization: nvidia, pypi: nemo-toolkit}
22 25 - {key: nanogpt, repo: karpathy/nanoGPT, kind: library}
23 − # ---------------------------------------------------------------- inference runtimes
24 − - {key: vllm, repo: vllm-project/vllm, kind: runtime, organization: vllm, pypi: vllm}
25 − - {key: llama-cpp, repo: ggml-org/llama.cpp, kind: runtime, organization: ggml}
26 + # ---------------------------------------------------------------- inference engines & runtimes
27 + - {key: vllm, repo: vllm-project/vllm, kind: inference-engine, organization: vllm, pypi: vllm}
28 + - {key: llama-cpp, repo: ggml-org/llama.cpp, kind: inference-engine, organization: ggml}
26 29 - {key: llama-cpp-python, repo: abetlen/llama-cpp-python, kind: library, pypi: llama-cpp-python}
27 − - {key: mlx, repo: ml-explore/mlx, kind: framework, organization: ml-explore, pypi: mlx}
28 − - {key: mlx-lm, repo: ml-explore/mlx-lm, kind: runtime, organization: ml-explore, pypi: mlx-lm}
29 − - {key: ollama, repo: ollama/ollama, kind: runtime, organization: ollama, pypi: ollama}
30 − - {key: sglang, repo: sgl-project/sglang, kind: runtime, organization: sglang, pypi: sglang}
31 − - {key: tensorrt-llm, repo: NVIDIA/TensorRT-LLM, kind: runtime, organization: nvidia, pypi: tensorrt-llm}
30 + - {key: mlx, repo: ml-explore/mlx, kind: training-framework, organization: ml-explore, pypi: mlx}
31 + - {key: mlx-lm, repo: ml-explore/mlx-lm, kind: inference-engine, organization: ml-explore, pypi: mlx-lm}
32 + - {key: ollama, repo: ollama/ollama, kind: inference-engine, organization: ollama, pypi: ollama}
33 + - {key: sglang, repo: sgl-project/sglang, kind: inference-engine, organization: sglang, pypi: sglang}
34 + - {key: tensorrt-llm, repo: NVIDIA/TensorRT-LLM, kind: inference-engine, organization: nvidia, pypi: tensorrt-llm}
32 35 - {key: onnxruntime, repo: microsoft/onnxruntime, kind: runtime, organization: microsoft, pypi: onnxruntime}
33 − - {key: text-generation-inference, repo: huggingface/text-generation-inference, kind: runtime, organization: huggingface}
34 − - {key: candle, repo: huggingface/candle, kind: framework, organization: huggingface}
35 − - {key: mlc-llm, repo: mlc-ai/mlc-llm, kind: runtime, organization: mlc-ai} # wheels are not on PyPI (mlc.ai index)
36 − - {key: exllamav2, repo: turboderp-org/exllamav2, kind: runtime, organization: turboderp, pypi: exllamav2}
37 − - {key: exllamav3, repo: turboderp-org/exllamav3, kind: runtime, organization: turboderp}
38 − - {key: lmdeploy, repo: InternLM/lmdeploy, kind: runtime, organization: internlm, pypi: lmdeploy}
39 − - {key: exo, repo: exo-explore/exo, kind: runtime, organization: exo-labs}
36 + - {key: text-generation-inference, repo: huggingface/text-generation-inference, kind: serving-engine, organization: huggingface}
37 + - {key: candle, repo: huggingface/candle, kind: training-framework, organization: huggingface}
38 + - {key: mlc-llm, repo: mlc-ai/mlc-llm, kind: inference-engine, organization: mlc-ai} # wheels are not on PyPI (mlc.ai index)
39 + - {key: exllamav2, repo: turboderp-org/exllamav2, kind: inference-engine, organization: turboderp, pypi: exllamav2}
40 + - {key: exllamav3, repo: turboderp-org/exllamav3, kind: inference-engine, organization: turboderp}
41 + - {key: lmdeploy, repo: InternLM/lmdeploy, kind: inference-engine, organization: internlm, pypi: lmdeploy}
42 + - {key: exo, repo: exo-explore/exo, kind: inference-engine, organization: exo-labs}
40 43 - {key: lms, repo: lmstudio-ai/lms, kind: tool, organization: lm-studio}
41 − - {key: text-generation-webui, repo: oobabooga/textgen, kind: tool}
42 − - {key: comfyui, repo: Comfy-Org/ComfyUI, kind: tool, organization: comfy-org}
43 − - {key: stable-diffusion-webui, repo: AUTOMATIC1111/stable-diffusion-webui, kind: tool}
44 + - {key: text-generation-webui, repo: oobabooga/textgen, kind: application}
45 + - {key: comfyui, repo: Comfy-Org/ComfyUI, kind: application, organization: comfy-org}
46 + - {key: stable-diffusion-webui, repo: AUTOMATIC1111/stable-diffusion-webui, kind: application}
44 47 # ---------------------------------------------------------------- SDKs, orchestration & agents
45 − - {key: openai-python, repo: openai/openai-python, kind: library, organization: openai, pypi: openai}
46 − - {key: anthropic-sdk-python, repo: anthropics/anthropic-sdk-python, kind: library, organization: anthropic, pypi: anthropic}
47 − - {key: google-genai-python, repo: googleapis/python-genai, kind: library, organization: google, pypi: google-genai}
48 − - {key: gemini-cli, repo: google-gemini/gemini-cli, kind: agent, organization: google}
49 − - {key: mcp-servers, repo: modelcontextprotocol/servers, kind: tool, organization: model-context-protocol}
50 − - {key: langchain, repo: langchain-ai/langchain, kind: framework, organization: langchain, pypi: langchain}
51 − - {key: langgraph, repo: langchain-ai/langgraph, kind: framework, organization: langchain, pypi: langgraph}
52 − - {key: llama-index, repo: run-llama/llama_index, kind: framework, organization: llamaindex, pypi: llama-index}
53 − - {key: litellm, repo: BerriAI/litellm, kind: library, organization: berriai, pypi: litellm}
54 − - {key: codex, repo: openai/codex, kind: agent, organization: openai}
55 − - {key: claude-code, repo: anthropics/claude-code, kind: agent, organization: anthropic}
56 − - {key: openhands, repo: OpenHands/OpenHands, kind: agent, organization: all-hands-ai, pypi: openhands-ai}
57 − - {key: aider, repo: Aider-AI/aider, kind: agent, organization: aider, pypi: aider-chat}
58 − - {key: crewai, repo: crewAIInc/crewAI, kind: framework, organization: crewai, pypi: crewai}
59 − - {key: autogen, repo: microsoft/autogen, kind: framework, organization: microsoft, pypi: autogen-agentchat}
60 − - {key: pydantic-ai, repo: pydantic/pydantic-ai, kind: framework, organization: pydantic, pypi: pydantic-ai}
61 − - {key: browser-use, repo: browser-use/browser-use, kind: agent, organization: browser-use, pypi: browser-use}
48 + - {key: openai-python, repo: openai/openai-python, kind: sdk, organization: openai, pypi: openai}
49 + - {key: anthropic-sdk-python, repo: anthropics/anthropic-sdk-python, kind: sdk, organization: anthropic, pypi: anthropic}
50 + - {key: google-genai-python, repo: googleapis/python-genai, kind: sdk, organization: google, pypi: google-genai}
51 + - {key: gemini-cli, repo: google-gemini/gemini-cli, kind: agent, agent_kind: coding, organization: google}
52 + - {key: mcp-servers, repo: modelcontextprotocol/servers, kind: mcp-server, organization: model-context-protocol}
53 + - {key: langchain, repo: langchain-ai/langchain, kind: agent-framework, organization: langchain, pypi: langchain}
54 + - {key: langgraph, repo: langchain-ai/langgraph, kind: agent-framework, organization: langchain, pypi: langgraph}
55 + - {key: llama-index, repo: run-llama/llama_index, kind: agent-framework, organization: llamaindex, pypi: llama-index}
56 + - {key: litellm, repo: BerriAI/litellm, kind: sdk, organization: berriai, pypi: litellm}
57 + - {key: codex, repo: openai/codex, kind: agent, agent_kind: coding, organization: openai}
58 + - {key: claude-code, repo: anthropics/claude-code, kind: agent, agent_kind: coding, organization: anthropic}
59 + - {key: openhands, repo: OpenHands/OpenHands, kind: agent, agent_kind: coding, organization: all-hands-ai, pypi: openhands-ai}
60 + - {key: aider, repo: Aider-AI/aider, kind: agent, agent_kind: coding, organization: aider, pypi: aider-chat}
61 + - {key: crewai, repo: crewAIInc/crewAI, kind: agent-framework, organization: crewai, pypi: crewai}
62 + - {key: autogen, repo: microsoft/autogen, kind: agent-framework, organization: microsoft, pypi: autogen-agentchat}
63 + - {key: pydantic-ai, repo: pydantic/pydantic-ai, kind: agent-framework, organization: pydantic, pypi: pydantic-ai}
64 + - {key: browser-use, repo: browser-use/browser-use, kind: agent, agent_kind: browser, organization: browser-use, pypi: browser-use}
62 65 # ---------------------------------------------------------------- vector databases & retrieval
63 66 - {key: faiss, repo: facebookresearch/faiss, kind: library, organization: meta-ai, pypi: faiss-cpu}
64 − - {key: chroma, repo: chroma-core/chroma, kind: tool, organization: chroma, pypi: chromadb}
65 − - {key: qdrant, repo: qdrant/qdrant, kind: tool, organization: qdrant, pypi: qdrant-client}
66 − - {key: milvus, repo: milvus-io/milvus, kind: tool, organization: zilliz, pypi: pymilvus}
67 − - {key: weaviate, repo: weaviate/weaviate, kind: tool, organization: weaviate, pypi: weaviate-client}
68 − - {key: lancedb, repo: lancedb/lancedb, kind: tool, organization: lancedb, pypi: lancedb}
67 + - {key: chroma, repo: chroma-core/chroma, kind: vector-database, organization: chroma, pypi: chromadb}
68 + - {key: qdrant, repo: qdrant/qdrant, kind: vector-database, organization: qdrant, pypi: qdrant-client}
69 + - {key: milvus, repo: milvus-io/milvus, kind: vector-database, organization: zilliz, pypi: pymilvus}
70 + - {key: weaviate, repo: weaviate/weaviate, kind: vector-database, organization: weaviate, pypi: weaviate-client}
71 + - {key: lancedb, repo: lancedb/lancedb, kind: vector-database, organization: lancedb, pypi: lancedb}
69 72 # ---------------------------------------------------------------- evaluation & observability
70 − - {key: lm-evaluation-harness, repo: EleutherAI/lm-evaluation-harness, kind: tool, organization: eleutherai, pypi: lm-eval}
71 − - {key: openai-evals, repo: openai/evals, kind: tool, organization: openai}
72 − - {key: helm, repo: stanford-crfm/helm, kind: tool, organization: stanford-crfm, pypi: crfm-helm}
73 − - {key: lighteval, repo: huggingface/lighteval, kind: tool, organization: huggingface, pypi: lighteval}
74 − - {key: ragas, repo: vibrantlabsai/ragas, kind: library, organization: vibrant-labs, pypi: ragas}
75 − - {key: langfuse, repo: langfuse/langfuse, kind: tool, organization: langfuse, pypi: langfuse}
76 − - {key: phoenix, repo: Arize-ai/phoenix, kind: tool, organization: arize, pypi: arize-phoenix}
73 + - {key: lm-evaluation-harness, repo: EleutherAI/lm-evaluation-harness, kind: evaluation-harness, organization: eleutherai, pypi: lm-eval}
74 + - {key: openai-evals, repo: openai/evals, kind: evaluation-harness, organization: openai}
75 + - {key: helm, repo: stanford-crfm/helm, kind: evaluation-harness, organization: stanford-crfm, pypi: crfm-helm}
76 + - {key: lighteval, repo: huggingface/lighteval, kind: evaluation-harness, organization: huggingface, pypi: lighteval}
77 + - {key: ragas, repo: vibrantlabsai/ragas, kind: evaluation-harness, organization: vibrant-labs, pypi: ragas}
78 + - {key: langfuse, repo: langfuse/langfuse, kind: observability, organization: langfuse, pypi: langfuse}
79 + - {key: phoenix, repo: Arize-ai/phoenix, kind: observability, organization: arize, pypi: arize-phoenix}
77 80 # ---------------------------------------------------------------- model code releases
78 81 - {key: whisper, repo: openai/whisper, kind: model, organization: openai, pypi: openai-whisper}
79 82 - {key: gpt-oss, repo: openai/gpt-oss, kind: model, organization: openai, pypi: gpt-oss}
added src/aiatlas/connectors/_identity.py +387 −0
@@ -0,0 +1,387 @@
1 +"""Shared model-identity helper for third-party sources (leaderboards, aggregators, provider price lists).
2 +
3 +Those sources name models with *API ids* (`anthropic/claude-3-7-sonnet-20250219`, `gemini/gemini-2.5-pro-preview-05-06`,
4 +`openrouter/x-ai/grok-4`, `gpt-4o-2024-08-06`, `Qwen/Qwen2.5-Coder-32B-Instruct`) or free text. This module turns such an id into:
5 +
6 + * the developer organisation (registry `org_ref`) when the id carries a first-party prefix or family word — never a guess beyond
7 + the deterministic tables below;
8 + * resolver-friendly aliases (the raw id, the id without provider prefix, the id without evaluator effort suffixes);
9 + * identifiers, **only when the caller marks the id as trusted** (= the string really is the vendor's own API id, e.g. the `--model`
10 + argument aider passed to the API). Free-text tags never become identifiers: the resolver refuses to merge an alias match when the
11 + entity already carries a *different* value for the same scheme, so a wrong `anthropic_model_id` would split an entity instead of
12 + linking it.
13 + * the evaluation-effort configuration encoded in the id or its label ("(xhigh)", "-thinking-64k", "(Non-reasoning)") — effort variants
14 + are result *configurations*, never model entities.
15 +"""
16 +from __future__ import annotations
17 +
18 +import re
19 +from dataclasses import dataclass, field
20 +from typing import Any
21 +
22 +from aiatlas.ontology.models import OFFICIAL_THINKING_RELEASES, analyze_model_name, family_release_hint
23 +from aiatlas.registry import org_by_hf, org_ref, organizations
24 +from aiatlas.sdk.facts import EntityRef, Facts
25 +
26 +# provider / gateway prefixes used by litellm, aider, OpenRouter, LiveBench… → (identifier scheme for the remainder, developer org key)
27 +# scheme None = the prefix is a gateway or an OpenAI-compatible endpoint, the remainder decides.
28 +PROVIDER_PREFIXES: dict[str, tuple[str | None, str | None]] = {
29 + "anthropic": ("anthropic_model_id", "anthropic"),
30 + "gemini": ("gemini_model_id", "google"),
31 + "google": ("gemini_model_id", "google"),
32 + "vertex_ai": (None, "google"),
33 + "deepseek": ("deepseek_model_id", "deepseek"),
34 + "mistral": ("mistral_model_id", "mistral"),
35 + "mistralai": ("mistral_model_id", "mistral"),
36 + "xai": ("xai_model_id", "xai"),
37 + "x-ai": ("xai_model_id", "xai"),
38 + "cohere": ("cohere_model_id", "cohere"),
39 + "groq": ("groq_model_id", None),
40 + "together_ai": ("together_ai_model_slug", None),
41 + "fireworks_ai": ("fireworks_model_id", None),
42 + "openai": (None, None), # litellm/aider route *any* OpenAI-compatible endpoint through `openai/…`
43 + "azure": (None, None),
44 + "bedrock": (None, None),
45 + "nvidia_nim": (None, None),
46 + "openrouter": (None, None),
47 +}
48 +# OpenRouter vendor slug → registry organisation (shared with the OpenRouter connector)
49 +VENDOR_ORG: dict[str, str] = {
50 + "meta": "meta-ai", "meta-llama": "meta-ai", "liquid": "liquid-ai", "x-ai": "xai", "mistralai": "mistral", "z-ai": "zhipu", "thudm": "zhipu",
51 + "moonshotai": "moonshot", "bytedance": "bytedance", "bytedance-seed": "bytedance", "ibm-granite": "ibm", "google": "google",
52 + "amazon": "amazon", "qwen": "qwen", "deepseek": "deepseek", "anthropic": "anthropic", "openai": "openai", "cohere": "cohere",
53 + "nvidia": "nvidia", "microsoft": "microsoft", "perplexity": "perplexity", "minimax": "minimax", "tencent": "tencent", "baidu": "baidu",
54 + "ai21": "ai21", "nousresearch": "nous-research", "openrouter": "openrouter", "all-hands": "all-hands-ai", "stepfun": "stepfun",
55 + "stepfun-ai": "stepfun", "upstage": "upstage", "kwaipilot": "kwaipilot", "thinking-machines": "thinking-machines", "thinkingmachines": "thinking-machines",
56 + "allenai": "allenai", "eleutherai": "eleutherai", "internlm": "internlm",
57 +}
58 +# first-party naming patterns of bare API ids → (identifier scheme or None, developer org key). Ordered: specific families first.
59 +_BARE: list[tuple[re.Pattern[str], str | None, str | None]] = [
60 + (re.compile(r"^claude[-_ ]"), "anthropic_model_id", "anthropic"),
61 + (re.compile(r"^(chatgpt-|gpt-(?!oss)|o[1-9](-|$)|codex|davinci|text-embedding-|dall-e|sora|tts-1|computer-use-preview)"), "openai_model_id", "openai"),
62 + (re.compile(r"^gpt-oss"), None, "openai"),
63 + (re.compile(r"^(gemini-|imagen-|veo-|lyria-|gemini-embedding)"), "gemini_model_id", "google"),
64 + (re.compile(r"^(gemma-|paligemma|medgemma|shieldgemma)"), None, "google"),
65 + (re.compile(r"^grok-"), "xai_model_id", "xai"),
66 + (re.compile(r"^deepseek-"), "deepseek_model_id", "deepseek"),
67 + (re.compile(r"^(mistral-|mixtral-|ministral-|codestral|magistral|devstral|pixtral|voxtral|open-mistral|open-mixtral|mistral-embed)"), "mistral_model_id", "mistral"),
68 + (re.compile(r"^(command-|command$|c4ai-|embed-(english|multilingual|v)|rerank-)"), "cohere_model_id", "cohere"),
69 + (re.compile(r"^aya-"), None, "cohere"),
70 + (re.compile(r"^(hermes)"), None, "nous-research"),
71 + (re.compile(r"^(llama[- ]?\d[\d.]*[- ].*nemotron|nemotron|nvidia-)"), None, "nvidia"),
72 + (re.compile(r"^(meta-)?llama-\d"), None, "meta-ai"),
73 + (re.compile(r"^(qwen|qwq|qvq)"), None, "qwen"),
74 + (re.compile(r"^kimi-"), None, "moonshot"),
75 + (re.compile(r"^(glm-|chatglm)"), None, "zhipu"),
76 + (re.compile(r"^minimax-"), None, "minimax"),
77 + (re.compile(r"^nova-"), None, "amazon"),
78 + (re.compile(r"^phi-"), None, "microsoft"),
79 + (re.compile(r"^sonar"), None, "perplexity"),
80 + (re.compile(r"^jamba"), None, "ai21"),
81 + (re.compile(r"^granite-"), None, "ibm"),
82 + (re.compile(r"^hunyuan"), None, "tencent"),
83 + (re.compile(r"^ernie"), None, "baidu"),
84 + (re.compile(r"^(doubao|seed-)"), None, "bytedance"),
85 + (re.compile(r"^lfm"), None, "liquid-ai"),
86 + (re.compile(r"^step-?\d"), None, "stepfun"),
87 + (re.compile(r"^solar-"), None, "upstage"),
88 + (re.compile(r"^(kat-|kwaipilot)"), None, "kwaipilot"),
89 + (re.compile(r"^dbrx"), None, "databricks"),
90 + (re.compile(r"^(olmo|molmo|tulu)"), None, "allenai"),
91 + (re.compile(r"^(smollm|smolvlm)"), None, "huggingface"),
92 +]
93 +_FIREWORKS_PATH = re.compile(r"^accounts/fireworks/models/")
94 +_DATE_IN_ID = re.compile(r"(20\d{2}-?(0[1-9]|1[0-2])-?(0[1-9]|[12]\d|3[01]))|(?<![0-9])(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])(?![0-9])|(exp|preview)-\d{2}-\d{2}")
95 +# compound evaluator suffixes that the ontology table does not know (LiveBench): "-thinking-auto" = adaptive thinking budget
96 +_EXTRA_SUFFIXES: dict[str, dict[str, str]] = {"thinking-auto": {"reasoning": "on", "thinking_budget": "auto"}, "auto": {"thinking_budget": "auto"},
97 + "effort": {}}
98 +_EFFORT_WORDS = re.compile(r"^(x-?high|extra-high|high|medium|low|minimal|max|effort|thinking|non-?reasoning|no-?think(ing)?|reasoning|auto|\d+k|default|\(.*\))$", re.IGNORECASE)
99 +_LABEL_EFFORT = re.compile(r"^(?P<eff>x-?high|extra-high|high|medium|low|minimal|max)(?:\s+effort)?(?:\s+with\s+fallback)?$", re.IGNORECASE)
100 +_LABEL_BUDGET = re.compile(r"^(?P<k>\d+)k\s*(think(ing)?|reasoning)?(\s*tokens)?$", re.IGNORECASE)
101 +_LABEL_OFF = re.compile(r"^(non-?reasoning|no-?\s?think(ing)?|thinking\s*off|non-?thinking|without\s+thinking)$", re.IGNORECASE)
102 +_LABEL_ON = re.compile(r"^(thinking|reasoning|think|thinking\s*on|reasoner)$", re.IGNORECASE)
103 +_LABEL_DEFAULT = re.compile(r"^(default(\s+think(ing)?)?)$", re.IGNORECASE)
104 +_LABEL_ADAPTIVE = re.compile(r"^(adaptive(\s+(thinking|reasoning))?|thinking\s+auto|auto)$", re.IGNORECASE)
105 +_PAREN = re.compile(r"\s*\(([^()]*)\)\s*$")
106 +
107 +
108 +@dataclass
109 +class ModelIdentity:
110 + raw: str
111 + base_id: str # id without provider prefix, variant suffix and effort suffixes
112 + scheme: str | None = None # identifier scheme the id belongs to (when recognised)
113 + identifiers: dict[str, str] = field(default_factory=dict)
114 + aliases: list[str] = field(default_factory=list)
115 + org_key: str | None = None # registry organisation key of the developer
116 + effort: dict[str, str] = field(default_factory=dict)
117 + provider_prefix: str | None = None
118 + pinned: bool = False # the id names a dated snapshot (stable), not a rolling alias
119 +
120 + @property
121 + def is_effort_variant(self) -> bool:
122 + return bool(self.effort)
123 +
124 +
125 +# ---------------------------------------------------------------------------------------------- effort labels / suffixes
126 +def effort_from_label(label: str | None) -> dict[str, str] | None:
127 + """'(xhigh)' → {reasoning_effort: xhigh} · '(Non-reasoning)' → {reasoning: off} · '(32k thinking tokens)' → {reasoning: on,
128 + thinking_budget: 32k} · '(default think)' → {thinking_budget: default}. None when the label is not an evaluation setting."""
129 + if not label:
130 + return None
131 + s = label.strip().strip("()").strip().replace("_", "-")
132 + if not s:
133 + return None
134 + m = _LABEL_EFFORT.match(s)
135 + if m:
136 + eff = m.group("eff").lower().replace("x-high", "xhigh").replace("extra-high", "xhigh")
137 + out = {"reasoning_effort": eff}
138 + if "fallback" in s.lower():
139 + out["effort_fallback"] = "on"
140 + return out
141 + m = _LABEL_BUDGET.match(s)
142 + if m:
143 + return {"reasoning": "on", "thinking_budget": f"{m.group('k')}k"}
144 + if _LABEL_OFF.match(s):
145 + return {"reasoning": "off"}
146 + if _LABEL_ON.match(s):
147 + return {"reasoning": "on"}
148 + if _LABEL_ADAPTIVE.match(s):
149 + return {"reasoning": "adaptive"}
150 + if _LABEL_DEFAULT.match(s):
151 + return {"thinking_budget": "default"}
152 + return None
153 +
154 +
155 +def split_effort_label(name: str | None) -> tuple[str, dict[str, str]]:
156 + """'Claude Opus 5 (xhigh)' → ('Claude Opus 5', {reasoning_effort: xhigh}); 'GPT-5.6 Sol xHigh Effort' → ('GPT-5.6 Sol', {reasoning_effort:
157 + xhigh}); 'Mistral Small 4 Non-reasoning' → reasoning off. Labels that are not settings ("(0324)", "(Jul)", "Thinking" alone) stay in the name."""
158 + if not name:
159 + return "", {}
160 + n = name.strip()
161 + m = _PAREN.search(n)
162 + if m:
163 + eff = effort_from_label(m.group(1))
164 + if eff is not None:
165 + return n[: m.start()].strip(), eff
166 + m = _TRAILING_EFFORT.search(n)
167 + if m:
168 + eff = m.group("eff").lower().replace("x-high", "xhigh").replace("extra-high", "xhigh")
169 + return n[: m.start()].strip(), {"reasoning_effort": eff}
170 + m = _TRAILING_OFF.search(n)
171 + if m:
172 + return n[: m.start()].strip(), {"reasoning": "off"}
173 + return n, {}
174 +
175 +
176 +# Suffixes that also name size tiers or official products ("mistral-medium", "sonar-reasoning"): they count as an evaluator setting only
177 +# when the remaining stem still carries a version/size digit ("o3-mini-high", "deepseek-v3-1-reasoning"). ("max", "fast", "instant" are
178 +# not effort suffixes at all in the ontology — Qwen3-Max, Grok 4.1 Fast and Claude Instant are model tiers.)
179 +_AMBIGUOUS_SUFFIXES = {"medium", "low", "high", "reasoning", "think"}
180 +_TRAILING_EFFORT = re.compile(r"\s+(?P<eff>x-?high|extra-high|high|medium|low|minimal|max)\s+effort$", re.IGNORECASE)
181 +_TRAILING_OFF = re.compile(r"\s+(non-?reasoning|no-?think(ing)?|non-?thinking)$", re.IGNORECASE)
182 +
183 +
184 +def strip_effort(model_id: str) -> tuple[str, dict[str, str]]:
185 + """Remove every trailing evaluator effort suffix of an id ('…-thinking-64k-high-effort' → '…', {reasoning: on, thinking_budget: 64k,
186 + reasoning_effort: high}). Official '-thinking' releases (ontology list) are left alone."""
187 + low = re.sub(r"[\s_]+", "-", model_id.strip().lower())
188 + effort: dict[str, str] = {}
189 + for _ in range(4):
190 + if low in OFFICIAL_THINKING_RELEASES:
191 + break
192 + a = analyze_model_name(low)
193 + if a.is_effort_variant and a.effort_suffix:
194 + stem = low[: -(len(a.effort_suffix) + 1)]
195 + if a.effort_suffix in _AMBIGUOUS_SUFFIXES and not re.search(r"\d", stem):
196 + break
197 + effort = {**a.effort, **effort}
198 + low = stem
199 + continue
200 + hit = next((s for s in sorted(_EXTRA_SUFFIXES, key=len, reverse=True) if low.endswith("-" + s)), None)
201 + if hit:
202 + effort = {**_EXTRA_SUFFIXES[hit], **effort}
203 + low = low[: -(len(hit) + 1)]
204 + continue
205 + break
206 + # keep the original casing of the surviving prefix
207 + base = model_id.strip()[: len(low)] if re.sub(r"[\s_]+", "-", model_id.strip().lower()).startswith(low) else low
208 + return base.rstrip("-_ "), effort
209 +
210 +
211 +def strip_effort_words(display: str, *, only_if: bool = True) -> str:
212 + """Trailing effort words of a display name ('Claude 4.6 Opus Thinking High Effort' → 'Claude 4.6 Opus'). Applied only when the
213 + caller knows (from the id) that the row is an effort variant — 'Kimi K2 Thinking' and 'Sonar Reasoning' are real releases."""
214 + if not only_if:
215 + return display.strip()
216 + words = display.strip().split()
217 + while len(words) > 1 and _EFFORT_WORDS.match(words[-1]):
218 + words.pop()
219 + return " ".join(words)
220 +
221 +
222 +def is_pinned(model_id: str) -> bool:
223 + return bool(_DATE_IN_ID.search(model_id))
224 +
225 +
226 +# ---------------------------------------------------------------------------------------------- identity
227 +def org_key_for_vendor(vendor: str | None) -> str | None:
228 + if not vendor:
229 + return None
230 + v = vendor.strip().lower()
231 + key = VENDOR_ORG.get(v)
232 + if key and key in organizations():
233 + return key
234 + known = org_by_hf(v)
235 + if known:
236 + return known["key"]
237 + if v in organizations():
238 + return v
239 + return None
240 +
241 +
242 +def org_key_for_bare_id(model_id: str) -> tuple[str | None, str | None]:
243 + low = model_id.strip().lower()
244 + for pat, scheme, org in _BARE:
245 + if pat.search(low):
246 + return scheme, (org if org in organizations() else None)
247 + return None, None
248 +
249 +
250 +def model_identity(api_id: str | None, *, trusted: bool = False) -> ModelIdentity | None:
251 + """Analyse an API id. `trusted=True` when the string is the vendor's own API id (adds the vendor identifier when recognised)."""
252 + if not api_id or not isinstance(api_id, str):
253 + return None
254 + raw = api_id.strip()
255 + if not raw or " " in raw:
256 + return None
257 + ident = ModelIdentity(raw=raw, base_id=raw)
258 + rest = raw
259 + prefix, _, tail = raw.partition("/")
260 + plow = prefix.lower()
261 + scheme: str | None = None
262 + org_key: str | None = None
263 + if tail and plow in PROVIDER_PREFIXES:
264 + ident.provider_prefix = plow
265 + scheme, org_key = PROVIDER_PREFIXES[plow]
266 + rest = tail
267 + if plow == "openrouter":
268 + vendor, _, slug = rest.partition("/")
269 + if slug:
270 + base_slug = slug.split(":", 1)[0]
271 + ident.aliases += [f"{vendor}/{base_slug}", base_slug]
272 + if trusted:
273 + ident.identifiers["openrouter"] = f"{vendor}/{base_slug}"
274 + org_key = org_key_for_vendor(vendor)
275 + rest = base_slug
276 + elif plow == "fireworks_ai":
277 + rest = _FIREWORKS_PATH.sub("", rest)
278 + if trusted and "/" not in rest:
279 + ident.identifiers["fireworks_model_id"] = f"fireworks/{rest}"
280 + scheme = None
281 + elif plow in ("nvidia_nim",):
282 + vendor, _, slug = rest.partition("/")
283 + if slug:
284 + org_key = org_key_for_vendor(vendor)
285 + rest = slug
286 + elif plow == "openai":
287 + # `openai/Qwen/Qwen2.5-Coder-32B-Instruct` (OpenAI-compatible endpoint): the remainder decides
288 + bare_scheme, bare_org = org_key_for_bare_id(rest.split("/")[-1])
289 + if "/" in rest:
290 + hf_org = rest.split("/")[0]
291 + known = org_by_hf(hf_org)
292 + if known:
293 + org_key = known["key"]
294 + if trusted:
295 + ident.identifiers["hf_repo"] = rest
296 + ident.aliases.append(rest)
297 + rest = rest.split("/")[-1]
298 + else:
299 + scheme, org_key = bare_scheme, bare_org
300 + elif tail and "/" in raw and org_by_hf(prefix):
301 + # `Qwen/Qwen2.5-Coder-32B-Instruct` — a Hugging Face repository id
302 + org_key = org_by_hf(prefix)["key"] # type: ignore[index]
303 + if trusted:
304 + ident.identifiers["hf_repo"] = raw
305 + ident.aliases.append(raw)
306 + rest = tail
307 + if org_key is None:
308 + bare_scheme, bare_org = org_key_for_bare_id(rest)
309 + scheme = scheme or bare_scheme
310 + org_key = bare_org
311 + base, effort = strip_effort(rest)
312 + ident.base_id = base or rest
313 + ident.effort = effort
314 + ident.scheme = scheme
315 + ident.org_key = org_key if org_key in organizations() else None
316 + ident.pinned = is_pinned(ident.base_id)
317 + if scheme and trusted and "/" not in ident.base_id and not ident.identifiers:
318 + ident.identifiers[scheme] = ident.base_id
319 + for a in (raw, rest, ident.base_id):
320 + if a and a not in ident.aliases:
321 + ident.aliases.append(a)
322 + return ident
323 +
324 +
325 +def family_ref(name: str | None, org: EntityRef | None) -> EntityRef | None:
326 + """`model_family` hint for a model name ("Claude Opus 5" → Claude; "Qwen3.6-35B-A3B" → Qwen3.6). None when no family is detected."""
327 + if not name:
328 + return None
329 + # size tokens first ("Qwen3-0.6B" would otherwise read as version 3.0): 0.6B / 35B-A3B / 17B-128E
330 + bare = re.sub(r"[-_ ]?\d+(?:\.\d+)?\s?[bmt](?:[-_]?a\d+(?:\.\d+)?[bmt])?(?:[-_]?\d+e)?(?![a-z0-9])", "", name.split("/")[-1], flags=re.IGNORECASE)
331 + label = family_release_hint(bare or name)
332 + if not label:
333 + return None
334 + return EntityRef(entity_type="model_family", name=label, organization=org, identity_confidence="medium",
335 + identifiers={"family_key": re.sub(r"[^a-z0-9.]+", "-", label.lower()).strip("-") + (f"@{org.slug_hint}" if org and org.slug_hint else "")})
336 +
337 +
338 +def org_ref_in(facts: Facts, key: str | None) -> EntityRef | None:
339 + """Registry organisation appended once to `facts.entities`."""
340 + if not key or key not in organizations():
341 + return None
342 + for e in facts.entities:
343 + if e.identifiers.get("registry_org") == key:
344 + return e
345 + ref = org_ref(key)
346 + facts.entities.append(ref)
347 + return ref
348 +
349 +
350 +def model_ref_from_api_id(facts: Facts, api_id: str | None, *, name: str | None = None, trusted: bool = False, identity_confidence: str = "medium",
351 + organization: EntityRef | None = None, extra_aliases: list[str] | None = None, family: bool = True,
352 + attributes: dict[str, Any] | None = None) -> tuple[EntityRef, dict[str, str]]:
353 + """Model EntityRef for a third-party API id (deduplicated inside `facts`), plus the effort configuration stripped from the id/label.
354 + `name` is the source's display label (effort parentheticals are folded into the configuration); without it the base id is the name."""
355 + ident = model_identity(api_id, trusted=trusted)
356 + label, label_effort = split_effort_label(name) if name else ("", {})
357 + effort = {**(ident.effort if ident else {}), **label_effort}
358 + display = label or (ident.base_id if ident else (api_id or "").strip())
359 + if ident and ident.is_effort_variant and label and not label_effort:
360 + display = strip_effort_words(label, only_if=True)
361 + # the first-party pattern org (what the lab connectors use) wins over the board's own creator label, so that the resolver
362 + # disambiguates aliases with the same organisation the official source attached
363 + org = (org_ref_in(facts, ident.org_key) if ident else None) or organization
364 + identifiers = dict(ident.identifiers) if ident else {}
365 + aliases = [a for a in dict.fromkeys([*(ident.aliases if ident else []), *(extra_aliases or [])]) if a and a != display]
366 + # one ref per identity inside the document: same identifier, or same display name (a board's rows for one model share its label)
367 + for e in facts.entities:
368 + if e.entity_type != "model":
369 + continue
370 + if (identifiers and any(e.identifiers.get(k) == v for k, v in identifiers.items())) or e.name.lower() == display.lower():
371 + for k, v in identifiers.items():
372 + e.identifiers.setdefault(k, v)
373 + for a in aliases:
374 + if a not in e.aliases and a != e.name:
375 + e.aliases.append(a)
376 + if e.organization is None and org is not None:
377 + e.organization = org
378 + if e.family is None and family:
379 + e.family = family_ref(display, org)
380 + return e, effort
381 + ref = facts.entity("model", display[:200], identifiers=identifiers, aliases=aliases, organization=org, identity_confidence=identity_confidence,
382 + family=family_ref(display, org) if family else None, attributes=dict(attributes or {}))
383 + return ref, effort
384 +
385 +
386 +__all__ = ["PROVIDER_PREFIXES", "VENDOR_ORG", "ModelIdentity", "effort_from_label", "family_ref", "is_pinned", "model_identity", "model_ref_from_api_id",
387 + "org_key_for_bare_id", "org_key_for_vendor", "org_ref_in", "split_effort_label", "strip_effort", "strip_effort_words"]
modified src/aiatlas/connectors/benchmarks/leaderboards.py +178 −77
@@ -1,17 +1,22 @@
1 1 """Public leaderboards → append-only `benchmark_results` (tier 2). One connector class per leaderboard domain (the SDK binds one
2 2 `source_key` per connector, and each board has its own rate limit and provenance) — module kept as `leaderboards`.
3 3
4 − * aider_leaderboard raw polyglot_leaderboard.yml from the aider repository → `aider-polyglot` (pass_rate_2 + well-formed rate).
4 + * aider_leaderboard raw polyglot_leaderboard.yml from the aider repository → `aider-polyglot` (pass_rate_2) and the separate
5 + `aider-polyglot-well-formed` benchmark (percent_cases_well_formed); run group = run date.
5 6 * swebench_leaderboard https://www.swebench.com/ embeds `<script id="leaderboard-data">` (5 boards: Verified, Lite, Test, Multimodal,
6 − Multilingual) → results keyed by (model, benchmark, config {system, system_org, …}).
7 + Multilingual) → results keyed by (model, benchmark, config {system, system_org, model_tag, reasoning_effort…});
8 + trust `official-benchmark` when SWE-bench checked the submission, `community` otherwise.
7 9 * livebench_leaderboard https://livebench.ai/ is a React app; its bundle lists the release dates and fetches `./table_<date>.csv` +
8 10 `./categories_<date>.json` (plus a model metadata map with display names / HF links). The connector follows
9 − index → bundle → categories → table and emits category averages + the overall mean (LiveBench's own aggregation).
11 + index → bundle → categories → table and emits the overall mean on `livebench` and each category average on its
12 + own benchmark entity (`livebench-<category>`, family livebench) — never mixed metrics on one board.
10 13 * artificial_analysis https://artificialanalysis.ai/leaderboards/models — Next.js RSC flight payload (`self.__next_f.push`) with the full
11 14 model list: Intelligence Index (config {version}) + component evaluations mapped to registry benchmarks.
12 15
13 −Model names on leaderboards are free text → `EntityRef("model", name, aliases=[…])`; the resolver merges by normalized alias when the
14 −model exists and otherwise creates a low-tier entity.
16 +Model identity on leaderboards (`connectors/_identity.py`): names are free text or evaluator slugs → `EntityRef("model", …,
17 +identity_confidence="medium")` with resolver-friendly aliases and the developer organisation when the id/name is first-party.
18 +**Evaluation-effort variants are configurations, not models**: `claude-opus-5-xhigh`, "GPT-5.5 (xhigh)", `…-thinking-64k-high-effort`
19 +all point at the base model with `config.reasoning_effort` / `config.reasoning` / `config.thinking_budget` set.
15 20 """
16 21 from __future__ import annotations
17 22
@@ -25,6 +30,17 @@ from typing import Any
25 30
26 31 import yaml
27 32
33 +from aiatlas.connectors._identity import (
34 + family_ref,
35 + model_identity,
36 + model_ref_from_api_id,
37 + org_key_for_bare_id,
38 + org_ref_in,
39 + split_effort_label,
40 + strip_effort,
41 + strip_effort_words,
42 +)
43 +from aiatlas.ontology.benchmarks import trust_level
28 44 from aiatlas.registry import load, org_by_hf, org_ref, organizations
29 45 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
30 46 from aiatlas.sdk.extract.dates import parse_datetime
@@ -33,6 +49,9 @@ from aiatlas.sdk.fetch import FetchResult
33 49
34 50
35 51 def benchmark_ref(key: str) -> EntityRef | None:
52 + """Registry benchmark → EntityRef. `slug_hint` is the registry key; when an *organisation* already owns that slug (LiveBench the
53 + org vs `livebench` the benchmark) the resolver appends a collision suffix (`livebench-2` in production) — the identity is the
54 + `registry_benchmark` identifier, never the slug."""
36 55 b = next((b for b in load("benchmarks") if b["key"] == key), None)
37 56 if not b:
38 57 return None
@@ -60,9 +79,7 @@ def org_from_name(facts: Facts, *names: str | None) -> EntityRef | None:
60 79 if key:
61 80 if key == "meta" and "meta-ai" in organizations():
62 81 key = "meta-ai"
63 − ref = org_ref(key)
64 − facts.entities.append(ref)
65 − return ref
82 + return org_ref_in(facts, key)
66 83 return None
67 84
68 85
@@ -74,9 +91,13 @@ def _dt(v: Any) -> datetime | None:
74 91 return parse_datetime(v) if isinstance(v, str) else None
75 92
76 93
94 +def _clean(cfg: dict[str, Any]) -> dict[str, Any]:
95 + return {k: v for k, v in cfg.items() if v not in (None, "", [], {})}
96 +
97 +
77 98 class _Leaderboard(BaseConnector):
78 − version = "1"
79 − parser_version = "1"
99 + version = "2"
100 + parser_version = "2"
80 101 interval_seconds = 12 * 3600
81 102 min_interval_seconds = 6 * 3600
82 103 max_interval_seconds = 3 * 86400
@@ -86,14 +107,18 @@ class _Leaderboard(BaseConnector):
86 107
87 108
88 109 # ================================================================================================ Aider polyglot
110 +AIDER_SHORTCUTS = {"r1", "sonnet", "opus", "haiku", "4o", "4-turbo", "35turbo", "3", "flash", "deepseek", "gemini", "gemini-exp", "gemini-2.5-pro"}
111 +
112 +
89 113 class AiderLeaderboardConnector(_Leaderboard):
90 114 name = "aider_leaderboard"
91 115 label = "Aider polyglot coding leaderboard"
92 − description = "polyglot_leaderboard.yml (raw GitHub file of the aider repo): pass rate, well-formed rate, cost, edit format per run."
116 + description = "polyglot_leaderboard.yml (raw GitHub file of the aider repo): pass rate and well-formed rate per run, edit format, API id."
93 117 source_key = "aider.chat"
94 118 rate_per_min = 10
95 119 expected_min_records = 30
96 120 URL = "https://raw.githubusercontent.com/Aider-AI/aider/main/aider/website/_data/polyglot_leaderboard.yml"
121 + PAGE = "https://aider.chat/docs/leaderboards/"
97 122
98 123 async def discover(self, ctx: RunContext) -> list[Target]:
99 124 return [Target(url=self.URL, doc_type="leaderboard", key="aider", min_bytes=1000, priority=1, meta={"content_type": "text/yaml"})]
@@ -101,35 +126,41 @@ class AiderLeaderboardConnector(_Leaderboard):
101 126 async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:
102 127 facts = Facts()
103 128 bench = benchmark_ref("aider-polyglot")
129 + well_formed = benchmark_ref("aider-polyglot-well-formed")
104 130 if bench is None:
105 131 return facts
106 132 facts.entities.append(bench)
133 + if well_formed is not None:
134 + facts.entities.append(well_formed)
107 135 try:
108 136 rows = yaml.safe_load(res.text) or []
109 137 except yaml.YAMLError:
110 138 return facts
139 + trust = trust_level(self.source_key)
111 140 for row in rows:
112 141 if not isinstance(row, dict) or not row.get("model") or row.get("pass_rate_2") is None:
113 142 continue
114 − name = str(row["model"]).strip()
115 − # the `--model` id is NOT an alias: many display-name variants share one API id, which would make every alias lookup
116 − # ambiguous (the resolver then creates a new entity per run). It is kept in the result config instead.
117 − ref = facts.entity("model", name[:200])
118 − evaluated = _dt(row.get("date"))
119 − config = {k: row.get(k) for k in ("edit_format", "command", "test_cases", "versions", "dirname") if row.get(k) is not None}
143 + label = str(row["model"]).strip()
144 + if re.search(r"\s\+\s", label) or row.get("edit_format") == "architect":
145 + continue # "o3 (high) + gpt-4.1": a two-model architect/editor system, not one model
120 146 m = re.search(r"--model\s+(\S+)", str(row.get("command") or ""))
121 − if m:
122 − config["api_model_id"] = m.group(1)
123 − config["date"] = evaluated.date().isoformat() if evaluated else None
124 − if row.get("total_cost") is not None:
125 − config["total_cost_usd"] = row["total_cost"]
126 − if row.get("seconds_per_case") is not None:
127 − config["seconds_per_case"] = row["seconds_per_case"]
147 + api_id = m.group(1) if m else None
148 + trusted = bool(api_id) and api_id not in AIDER_SHORTCUTS
149 + # rolling ids (`deepseek/deepseek-chat`, `codestral-latest`) name whichever snapshot served that day: identifiers only for
150 + # pinned snapshots and namespaced (OpenRouter / hub) ids, aliases otherwise
151 + ident = model_identity(api_id, trusted=trusted) if trusted else None
152 + use_ids = bool(ident) and (ident.pinned or ident.provider_prefix == "openrouter" or "hf_repo" in ident.identifiers) # type: ignore[union-attr]
153 + ref, effort = model_ref_from_api_id(facts, api_id if trusted else None, name=label, trusted=use_ids, identity_confidence="medium")
154 + evaluated = _dt(row.get("date"))
155 + config = _clean({"edit_format": row.get("edit_format"), "api_model_id": api_id, "date": evaluated.date().isoformat() if evaluated else None,
156 + "reasoning_effort": row.get("reasoning_effort"), **effort})
157 + run_group = config.get("date")
128 158 facts.result(model=ref, benchmark=bench, score=float(row["pass_rate_2"]), metric="pass_rate_2", unit="%", config=config, evaluated_at=evaluated,
129 − source_url="https://aider.chat/docs/leaderboards/")
130 − if row.get("percent_cases_well_formed") is not None:
131 − facts.result(model=ref, benchmark=bench, score=float(row["percent_cases_well_formed"]), metric="percent_cases_well_formed", unit="%",
132 − config=config, evaluated_at=evaluated, source_url="https://aider.chat/docs/leaderboards/")
159 + source_url=self.PAGE, trust_level=trust, variant="polyglot", run_group=run_group)
160 + if well_formed is not None and row.get("percent_cases_well_formed") is not None:
161 + facts.result(model=ref, benchmark=well_formed, score=float(row["percent_cases_well_formed"]), metric="percent_cases_well_formed", unit="%",
162 + config={**config, "variant": "well-formed"}, evaluated_at=evaluated, source_url=self.PAGE, trust_level=trust, variant="well-formed",
163 + run_group=run_group)
133 164 facts.document_entity = bench
134 165 facts.document_title = "Aider polyglot leaderboard data"
135 166 return facts
@@ -137,19 +168,21 @@ class AiderLeaderboardConnector(_Leaderboard):
137 168
138 169 # ================================================================================================ SWE-bench
139 170 BOARD_KEYS = {"verified": "swe-bench-verified", "lite": "swe-bench-lite", "test": "swe-bench-full", "full": "swe-bench-full",
140 − "multimodal": "swe-bench-multimodal", "multilingual": "swe-bench-multilingual"}
171 + "multimodal": "swe-bench-multimodal", "multilingual": "swe-bench-multilingual", "pro": "swe-bench-pro"}
172 +NOT_A_MODEL = {"multiple", "undisclosed", "unknown", "n/a", "-", "—", "various", "proprietary", "custom", "internal"}
141 173
142 174
143 175 class SweBenchLeaderboardConnector(_Leaderboard):
144 176 name = "swebench_leaderboard"
145 177 label = "SWE-bench leaderboards (Verified, Lite, Full, Multimodal, Multilingual)"
146 − description = "Embedded leaderboard JSON of swebench.com: % resolved per (system, model) submission with dates and cost."
178 + description = "Embedded leaderboard JSON of swebench.com: % resolved per (system, model) submission with dates; trust follows the SWE-bench check mark."
147 179 source_key = "swebench.com"
148 180 rate_per_min = 10
149 181 expected_min_records = 50
182 + URL = "https://www.swebench.com/"
150 183
151 184 async def discover(self, ctx: RunContext) -> list[Target]:
152 − return [Target(url="https://www.swebench.com/", doc_type="leaderboard", key="swebench", min_bytes=20000, priority=1)]
185 + return [Target(url=self.URL, doc_type="leaderboard", key="swebench", min_bytes=20000, priority=1)]
153 186
154 187 async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:
155 188 facts = Facts()
@@ -177,35 +210,34 @@ class SweBenchLeaderboardConnector(_Leaderboard):
177 210 return
178 211 if re.search(r"\s(&|\+|and)\s", model_name): # "GPT-4o & Claude 3 Opus": a multi-model system, not one model
179 212 return
213 + if model_name.lower() in NOT_A_MODEL: # "Multiple", "Undisclosed": the submission does not name its model
214 + return
180 215 tags = [t for t in r.get("tags") or [] if isinstance(t, str)]
181 216 model_tags = [t.split(":", 1)[1].strip() for t in tags if t.lower().startswith("model:")]
217 + model_tag = model_tags[0] if model_tags else None
182 218 org = org_from_name(facts, r.get("model_org"))
183 − ref = facts.entity("model", model_name[:200], organization=org) # SWE-bench's "Model:" tag goes to config, not aliases (see aider)
219 + # the "Model:" tag is free text ("claude-4-5-opus", "gpt-5-2"): aliases and organisation only, never an identifier
220 + ref, effort = model_ref_from_api_id(facts, model_tag, name=model_name, trusted=False, identity_confidence="medium", organization=org)
184 221 evaluated = _dt(r.get("date"))
185 − config = {"board": board, "system": r.get("agent") or r.get("name"), "system_org": r.get("agent_org"), "open_source_system": r.get("os_system"),
186 − "checked_by_swebench": r.get("checked"), "submission": r.get("folder"), "date": evaluated.date().isoformat() if evaluated else None}
187 − if model_tags:
188 − config["model_tag"] = model_tags[0]
189 − if r.get("reasoning_effort"):
190 − config["reasoning_effort"] = r["reasoning_effort"]
191 − if isinstance(r.get("cost"), (int, float)):
192 − config["total_cost_usd"] = r["cost"]
193 − if isinstance(r.get("instance_cost"), (int, float)):
194 − config["cost_per_instance_usd"] = r["instance_cost"]
195 − facts.result(model=ref, benchmark=bench, score=float(resolved), metric="resolved", unit="%", config={k: v for k, v in config.items() if v is not None},
196 − evaluated_at=evaluated, source_url="https://www.swebench.com/")
222 + config = _clean({"board": board, "system": r.get("agent") or r.get("name"), "system_org": r.get("agent_org"), "open_source_system": r.get("os_system"),
223 + "checked_by_swebench": r.get("checked"), "submission": r.get("folder"), "date": evaluated.date().isoformat() if evaluated else None,
224 + "model_tag": model_tag, "reasoning_effort": r.get("reasoning_effort") or None, **effort})
225 + facts.result(model=ref, benchmark=bench, score=float(resolved), metric="resolved", unit="%", config=config, evaluated_at=evaluated, source_url=self.URL,
226 + trust_level=trust_level(self.source_key, config), variant=board, run_group=config.get("date"))
197 227
198 228
199 229 # ================================================================================================ LiveBench
200 230 RELEASES = re.compile(r'\[("20\d\d-\d\d-\d\d"(?:,"20\d\d-\d\d-\d\d")*)\]')
201 231 MODEL_META = re.compile(r'"([\w.:/-]+)":\{((?:[^{}]|\{[^{}]*\})*?displayName:"(?:[^{}]|\{[^{}]*\})*)\}')
202 232 META_FIELD = re.compile(r'(url|huggingface|organization|displayName):"([^"]*)"')
233 +CATEGORY_KEYS = {"Reasoning": "livebench-reasoning", "Coding": "livebench-coding", "Agentic Coding": "livebench-agentic-coding", "Mathematics": "livebench-mathematics",
234 + "Data Analysis": "livebench-data-analysis", "Language": "livebench-language", "IF": "livebench-if", "Instruction Following": "livebench-if"}
203 235
204 236
205 237 class LiveBenchLeaderboardConnector(_Leaderboard):
206 238 name = "livebench_leaderboard"
207 239 label = "LiveBench leaderboard"
208 − description = "Latest LiveBench release table (CSV + categories JSON discovered from the site bundle): category averages and overall mean."
240 + description = "Latest LiveBench release table (CSV + categories JSON discovered from the site bundle): overall mean and one benchmark per category."
209 241 source_key = "livebench.ai"
210 242 rate_per_min = 10
211 243 expected_min_records = 20
@@ -255,16 +287,36 @@ class LiveBenchLeaderboardConnector(_Leaderboard):
255 287 cats: dict[str, list[str]] = target.meta.get("categories") or {}
256 288 meta: dict[str, dict[str, Any]] = target.meta.get("models") or {}
257 289 evaluated = _dt(release)
290 + trust = trust_level(self.source_key)
291 + cat_benches: dict[str, EntityRef] = {}
292 + for cat in cats:
293 + ckey = CATEGORY_KEYS.get(cat) or f"livebench-{re.sub(r'[^a-z0-9]+', '-', cat.lower()).strip('-')}"
294 + cref = benchmark_ref(ckey)
295 + if cref is not None:
296 + cat_benches[cat] = cref
297 + facts.entities.append(cref)
258 298 for row in csv.DictReader(io.StringIO(text)):
259 299 mid = (row.get("model") or "").strip()
260 300 if not mid:
261 301 continue
262 302 info = meta.get(mid) or {}
263 − name = info.get("display_name") or mid
303 + base_id, effort = strip_effort(mid)
304 + display, label_effort = split_effort_label(info.get("display_name") or base_id) # "GPT-5.6 Sol xHigh Effort" → base + setting
305 + effort = {**effort, **label_effort}
306 + if effort and info.get("display_name"):
307 + display = strip_effort_words(display, only_if=True)
264 308 # no `hf_repo` identifier here: several LiveBench ids (effort variants, MLX conversions) share one display name and would
265 309 # glue unrelated hub repositories onto a single entity; the link is kept as a claim on the leaderboard row's model
266 310 org = org_from_name(facts, info.get("organization"))
267 − ref = facts.entity("model", name[:200], organization=org, aliases=[mid])
311 + if org is None:
312 + _scheme, org_key = org_key_for_bare_id(base_id)
313 + org = org_ref_in(facts, org_key)
314 + ref = next((e for e in facts.entities if e.entity_type == "model" and e.name.lower() == display.lower()), None)
315 + if ref is None:
316 + ref = facts.entity("model", display[:200], organization=org, aliases=[a for a in dict.fromkeys([base_id, mid]) if a != display],
317 + identity_confidence="medium", family=family_ref(display, org))
318 + elif mid not in ref.aliases:
319 + ref.aliases.append(mid)
268 320 hf = info.get("huggingface") or ""
269 321 m = re.search(r"huggingface\.co/([\w.-]+/[\w.-]+)", hf)
270 322 if m:
@@ -277,12 +329,16 @@ class LiveBenchLeaderboardConnector(_Leaderboard):
277 329 cat_avgs[cat] = round(fmean(vals), 3)
278 330 if not cat_avgs:
279 331 continue
280 − base_cfg = {"release": release, "livebench_model_id": mid, "aggregation": "mean of category averages; category = mean of its subtasks"}
332 + base_cfg = _clean({"release": release, "livebench_model_id": mid, "aggregation": "mean of category averages; category = mean of its subtasks", **effort})
281 333 facts.result(model=ref, benchmark=bench, score=round(fmean(cat_avgs.values()), 3), metric="global_average", unit="%", config=base_cfg,
282 − evaluated_at=evaluated, source_url=self.BASE)
334 + evaluated_at=evaluated, source_url=self.BASE, trust_level=trust, variant="global", run_group=release)
283 335 for cat, avg in cat_avgs.items():
284 − facts.result(model=ref, benchmark=bench, score=avg, metric=f"category:{cat}", unit="%", config={"release": release, "livebench_model_id": mid, "subtasks": cats[cat]},
285 − evaluated_at=evaluated, source_url=self.BASE)
336 + cref = cat_benches.get(cat)
337 + if cref is None:
338 + continue
339 + facts.result(model=ref, benchmark=cref, score=avg, metric="average score", unit="%",
340 + config=_clean({"release": release, "livebench_model_id": mid, "variant": cat, "subtasks": cats[cat], **effort}),
341 + evaluated_at=evaluated, source_url=self.BASE, trust_level=trust, variant=cat, run_group=release)
286 342 facts.document_entity = bench
287 343 facts.document_title = f"LiveBench {release}"
288 344
@@ -296,17 +352,20 @@ def _is_number(v: str) -> bool:
296 352
297 353
298 354 # ================================================================================================ Artificial Analysis
299 −AA_EVALS = {"gpqa": ("gpqa", "accuracy", "GPQA Diamond"), "hle": ("humanitys-last-exam", "accuracy", None), "aime25": ("aime-2025", "accuracy", None),
355 +# RSC field → (registry benchmark key, metric, variant label). GPQA on AA is the Diamond subset; τ²-Bench is the Telecom domain.
356 +AA_EVALS = {"gpqa": ("gpqa-diamond", "accuracy", "Diamond"), "hle": ("humanitys-last-exam", "accuracy", None), "aime25": ("aime-2025", "accuracy", None),
300 357 "livecodebench": ("livecodebench", "pass@1", None), "scicode": ("scicode", "accuracy", None), "ifbench": ("ifbench", "accuracy", None),
301 − "mmmuPro": ("mmmu-pro", "accuracy", None), "tau2": ("tau2-bench", "pass^1", None), "terminalbenchV40": ("terminal-bench", "accuracy", "v4.0"),
358 + "mmmuPro": ("mmmu-pro", "accuracy", None), "tau2": ("tau2-bench", "pass^1", "Telecom"), "terminalbenchV40": ("terminal-bench", "accuracy", "v4.0"),
302 359 "terminalbenchV21": ("terminal-bench", "accuracy", "v2.1"), "terminalbenchHard": ("terminal-bench", "accuracy", "hard")}
303 360 PUSH = re.compile(r'self\.__next_f\.push\(\[1,"(.*?)"\]\)\s*</script>', re.DOTALL)
361 +# model-level facts AA copies from the labs (same for every effort variant of a model) → written once per base model as `aa_*`
362 +AA_MODEL_CLAIMS = ("aa_release_date", "aa_openness", "aa_context_window", "aa_deprecated")
304 363
305 364
306 365 class ArtificialAnalysisConnector(_Leaderboard):
307 366 name = "artificial_analysis"
308 367 label = "Artificial Analysis — Intelligence Index and component evaluations"
309 − description = "Model leaderboard page (RSC payload): Intelligence Index with version, component evaluations, openness and release dates."
368 + description = "Model leaderboard page (RSC payload): Intelligence Index with version and component evaluations; effort variants fold into their base model."
310 369 source_key = "artificialanalysis.ai"
311 370 rate_per_min = 6
312 371 expected_min_records = 100
@@ -325,33 +384,41 @@ class ArtificialAnalysisConnector(_Leaderboard):
325 384 assert index
326 385 facts.entities.append(index)
327 386 benches = {k: benchmark_ref(k) for k, *_ in AA_EVALS.values()}
387 + trust = trust_level(self.source_key)
388 + written: set[tuple[int, str]] = set()
389 +
390 + def once(ref: EntityRef, prop: str, value: Any, **kw: Any) -> None:
391 + """One claim per (base model, property): several effort variants of a model repeat the same model-level facts."""
392 + if value in (None, "", []) or (id(ref), prop) in written:
393 + return
394 + written.add((id(ref), prop))
395 + facts.claim(ref, prop, value, **kw)
396 +
328 397 for slug, m in models.items():
329 − name = (m.get("name") or slug).strip()
330 − creator = m.get("creator") or {}
331 − creator_name = creator.get("name") or m.get("modelCreatorName")
332 − org = org_from_name(facts, creator.get("slug"), creator_name)
333 − if org is None and creator_name:
334 − org = facts.entity("company", creator_name, identifiers={"artificial_analysis_creator": creator.get("slug") or _slug(creator_name)})
335 − ref = facts.entity("model", name[:200], identifiers={"artificial_analysis": slug}, organization=org,
336 − aliases=[a for a in {slug, m.get("shortName")} if a and a != name])
398 + ref, effort = self._model(facts, slug, m, models)
337 399 # AA copies release date / openness / context / deprecation from the labs: recorded under `aa_*` so that a second-hand
338 400 # tier-2 source never supersedes another tier-2 source (hub/OpenRouter) every run; the results are AA's own data.
339 401 if m.get("releaseDate"):
340 − facts.claim(ref, "aa_release_date", str(m["releaseDate"])[:10])
402 + once(ref, "aa_release_date", str(m["releaseDate"])[:10])
341 403 if isinstance(m.get("isOpenWeights"), bool):
342 − facts.claim(ref, "aa_openness", "open-weights" if m["isOpenWeights"] else "proprietary")
343 − if isinstance(m.get("isReasoning"), bool):
344 − facts.claim(ref, "reasoning", m["isReasoning"])
345 − if m.get("deprecated") is True:
346 − facts.claim(ref, "aa_deprecated", True)
404 + once(ref, "aa_openness", "open-weights" if m["isOpenWeights"] else "proprietary")
405 + if m.get("deprecated") is True and not effort:
406 + once(ref, "aa_deprecated", True)
347 407 if isinstance(m.get("contextWindowTokens"), int) and m["contextWindowTokens"] > 0:
348 − facts.claim(ref, "aa_context_window", m["contextWindowTokens"], unit="tokens")
349 − if isinstance(m.get("medianOutputTokensPerSecond"), (int, float)):
350 − facts.claim(ref, "metric.aa_median_output_tokens_per_second", round(float(m["medianOutputTokensPerSecond"]), 1))
408 + once(ref, "aa_context_window", m["contextWindowTokens"], unit="tokens")
409 + if not effort:
410 + if isinstance(m.get("isReasoning"), bool):
411 + once(ref, "reasoning", m["isReasoning"])
412 + if isinstance(m.get("medianOutputTokensPerSecond"), (int, float)):
413 + once(ref, "metric.aa_median_output_tokens_per_second", round(float(m["medianOutputTokensPerSecond"]), 1))
414 + # the reasoning switch is part of every result's configuration (a base row and its "non-reasoning" sibling differ by it)
415 + if isinstance(m.get("isReasoning"), bool) and "reasoning" not in effort and "reasoning_effort" not in effort:
416 + effort["reasoning"] = "on" if m["isReasoning"] else "off"
351 417 ii = m.get("intelligenceIndex")
352 418 if isinstance(ii, (int, float)):
353 − cfg = {"version": version, "estimated": bool(m.get("intelligenceIndexIsEstimated")), "aa_slug": slug}
354 − facts.result(model=ref, benchmark=index, score=round(float(ii), 2), metric="index", unit="", config=cfg, source_url=self.URL)
419 + cfg = _clean({"version": version, "estimated": bool(m.get("intelligenceIndexIsEstimated")), "aa_slug": slug, **effort})
420 + facts.result(model=ref, benchmark=index, score=round(float(ii), 2), metric="index", unit="", config=cfg, source_url=self.URL, trust_level=trust,
421 + variant="index", run_group=version)
355 422 for field, (bkey, metric, variant) in AA_EVALS.items():
356 423 v = m.get(field)
357 424 bench = benches.get(bkey)
@@ -359,14 +426,46 @@ class ArtificialAnalysisConnector(_Leaderboard):
359 426 continue
360 427 if bench not in facts.entities:
361 428 facts.entities.append(bench)
362 − cfg = {"evaluator": "Artificial Analysis", "index_version": version, "aa_slug": slug}
363 − if variant:
364 − cfg["variant"] = variant
365 − facts.result(model=ref, benchmark=bench, score=round(float(v) * 100, 2), metric=metric, unit="%", config=cfg, source_url=self.URL)
429 + cfg = _clean({"evaluator": "Artificial Analysis", "index_version": version, "aa_slug": slug, "variant": variant, **effort})
430 + facts.result(model=ref, benchmark=bench, score=round(float(v) * 100, 2), metric=metric, unit="%", config=cfg, source_url=self.URL, trust_level=trust,
431 + variant=variant, run_group=version)
366 432 facts.document_entity = index
367 433 facts.document_title = f"Artificial Analysis models — Intelligence Index v{version}" if version else "Artificial Analysis models"
368 434 return facts
369 435
436 + def _model(self, facts: Facts, slug: str, m: dict[str, Any], models: dict[str, dict[str, Any]]) -> tuple[EntityRef, dict[str, str]]:
437 + """Base-model EntityRef + effort configuration for one AA row. AA's own base label is `shortName` without its parenthetical
438 + ("Claude Opus 5 (xhigh)" → "Claude Opus 5"); the slug carries the suffix (`claude-opus-5-xhigh`). The `artificial_analysis`
439 + identifier is attached only when AA lists the base slug itself; otherwise the base slug is an alias."""
440 + short = (m.get("shortName") or m.get("name") or slug).strip()
441 + label, label_effort = split_effort_label(short)
442 + base_slug, slug_effort = strip_effort(slug)
443 + effort = {**slug_effort, **label_effort}
444 + if slug_effort and not label_effort:
445 + label = strip_effort_words(label, only_if=True)
446 + variant = bool(effort) and base_slug != slug
447 + creator = m.get("creator") or {}
448 + creator_name = creator.get("name") or m.get("modelCreatorName")
449 + org = org_from_name(facts, creator.get("slug"), creator_name)
450 + if org is None and org_key_for_bare_id(base_slug)[1] is None and creator_name:
451 + # creator unknown to the registry and no first-party pattern: AA's own creator label becomes a company with AA's identifier
452 + existing = next((e for e in facts.entities if e.identifiers.get("artificial_analysis_creator") == (creator.get("slug") or _slug(creator_name))), None)
453 + org = existing or facts.entity("company", creator_name, identifiers={"artificial_analysis_creator": creator.get("slug") or _slug(creator_name)})
454 + identifiers: dict[str, str] = {}
455 + aliases: list[str] = []
456 + if not variant:
457 + identifiers["artificial_analysis"] = slug
458 + elif base_slug in models:
459 + identifiers["artificial_analysis"] = base_slug
460 + else:
461 + aliases.append(base_slug)
462 + aliases += [a for a in (short if not effort else None, m.get("shortName") if not effort else None) if a]
463 + ref, _ = model_ref_from_api_id(facts, base_slug, name=label or base_slug, trusted=False, identity_confidence="medium", organization=org,
464 + extra_aliases=aliases)
465 + for k, v in identifiers.items():
466 + ref.identifiers.setdefault(k, v)
467 + return ref, effort
468 +
370 469
371 470 def parse_rsc_models(html: str) -> tuple[dict[str, dict[str, Any]], str | None]:
372 471 """Concatenate the Next.js flight chunks, parse each `id:json` line and collect every object with `slug` + `intelligenceIndex`."""
@@ -400,4 +499,6 @@ def parse_rsc_models(html: str) -> tuple[dict[str, dict[str, Any]], str | None]:
400 499 return models, (version_m.group(1) if version_m else None)
401 500
402 501
502 +_ = org_ref # kept importable for tests/back-compat
503 +
403 504 CONNECTORS = [AiderLeaderboardConnector, SweBenchLeaderboardConnector, LiveBenchLeaderboardConnector, ArtificialAnalysisConnector]
modified src/aiatlas/connectors/code/github.py +44 −9
@@ -9,8 +9,10 @@ Per repository (tier 2):
9 9 + one RELEASE event per feed entry (dedupe key = release URL).
10 10 * https://raw.githubusercontent.com/<owner>/<name>/HEAD/README.md → archived text for later LLM summarisation (deterministic: title only).
11 11
12 −Entity type: `framework` for kind framework|runtime|library|agent|tool, `repository` otherwise (model code drops). Identifiers
13 −`{"github_repo": "owner/name"}` (+ `pypi` when the seed maps a package) so the PyPI connector merges into the same entity.
12 +Entity type follows the canonical kind (aiatlas.ontology.taxonomy.normalize_framework_kind): `agent` for agents (with `agent_kind`),
13 +`tool` for tool | application | mcp-server, `repository` for model-code releases, `framework` for everything else (libraries, SDKs,
14 +inference engines…). Identifiers `{"github_repo": "owner/name"}` (+ `pypi` when the seed maps a package) so the PyPI connector merges
15 +into the same entity. `kind` is the canonical value, `kind_raw` the seed label when it differed; `license` is the ontology key.
14 16 """
15 17 from __future__ import annotations
16 18
@@ -18,6 +20,8 @@ import re
18 20 from datetime import UTC
19 21 from typing import Any
20 22
23 +from aiatlas.ontology.licenses import normalize_license
24 +from aiatlas.ontology.taxonomy import normalize_framework_kind
21 25 from aiatlas.registry import load, org_by_github, org_ref, organizations
22 26 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
23 27 from aiatlas.sdk.extract.dates import parse_datetime
@@ -28,7 +32,7 @@ from aiatlas.sdk.fetch import FetchResult
28 32
29 33 GH = "https://github.com"
30 34 RAW = "https://raw.githubusercontent.com"
31 −FRAMEWORK_KINDS = {"framework", "runtime", "library", "agent", "tool"}
35 +TOOL_KINDS = {"tool", "application", "mcp-server"}
32 36 VERSION_TAG = re.compile(r"^v?(\d+(?:\.\d+)+(?:[-+.][0-9A-Za-z.]+)?)$")
33 37
34 38
@@ -36,10 +40,36 @@ def repo_entries() -> list[dict[str, Any]]:
36 40 return load("repositories")
37 41
38 42
43 +def entity_type_for(kind: str | None) -> str:
44 + """Canonical kind → entity type (agents and tools get their own surfaces; model code stays a repository)."""
45 + canon = normalize_framework_kind(kind) if kind else None
46 + if kind == "model":
47 + return "repository"
48 + if canon == "agent":
49 + return "agent"
50 + if canon in TOOL_KINDS:
51 + return "tool"
52 + return "framework"
53 +
54 +
55 +def claim_license(facts: Facts, ref: EntityRef, raw: str | None, *, prop: str = "license") -> str | None:
56 + if not raw:
57 + return None
58 + key = normalize_license(raw)
59 + facts.claim(ref, prop, key or raw)
60 + if key != raw:
61 + facts.claim(ref, f"{prop}_raw", raw)
62 + return key
63 +
64 +
39 65 def repo_entity(facts: Facts, entry: dict[str, Any]) -> EntityRef:
40 66 """One EntityRef per seed repository — shared by the GitHub and PyPI connectors so both resolve to the same row."""
41 67 owner, name = entry["repo"].split("/", 1)
42 − etype = "framework" if entry.get("kind") in FRAMEWORK_KINDS else "repository"
68 + kind = entry.get("kind")
69 + etype = entity_type_for(kind)
70 + for e in facts.entities:
71 + if e.entity_type == etype and e.identifiers.get("github_repo") == entry["repo"]:
72 + return e
43 73 ids = {"github_repo": entry["repo"]}
44 74 if entry.get("pypi"):
45 75 ids["pypi"] = entry["pypi"]
@@ -51,8 +81,13 @@ def repo_entity(facts: Facts, entry: dict[str, Any]) -> EntityRef:
51 81 if known:
52 82 org = org_ref(known["key"])
53 83 aliases = [entry["repo"], entry["key"]] + ([entry["pypi"]] if entry.get("pypi") else [])
54 − ref = facts.entity(etype, name, identifiers=ids, organization=org, aliases=[a for a in aliases if a != name], slug_hint=entry["key"])
55 − facts.claim(ref, "kind", entry.get("kind"))
84 + ref = facts.entity(etype, name, identifiers=ids, organization=org, aliases=[a for a in aliases if a != name], slug_hint=entry["key"], identity_confidence="high")
85 + canon = normalize_framework_kind(kind) if kind and kind != "model" else kind
86 + facts.claim(ref, "kind", canon or kind)
87 + if canon and canon != kind:
88 + facts.claim(ref, "kind_raw", kind)
89 + if etype == "agent" and entry.get("agent_kind"):
90 + facts.claim(ref, "agent_kind", entry["agent_kind"])
56 91 facts.claim(ref, "repository_url", f"{GH}/{entry['repo']}")
57 92 if entry.get("pypi"):
58 93 facts.claim(ref, "pypi", entry["pypi"])
@@ -67,8 +102,8 @@ class GitHubConnector(BaseConnector):
67 102 label = "GitHub — AI frameworks, runtimes, agents and model repositories"
68 103 description = "Repository pages, release feeds and raw READMEs of the seed list in registry/repositories.yaml (HTML only, no API)."
69 104 source_key = "github.com"
70 − version = "1"
71 − parser_version = "1"
105 + version = "2"
106 + parser_version = "2"
72 107 interval_seconds = 12 * 3600
73 108 min_interval_seconds = 6 * 3600
74 109 max_interval_seconds = 3 * 86400
@@ -148,7 +183,7 @@ class GitHubConnector(BaseConnector):
148 183 if not spdx:
149 184 node = next((n for n in html.css("[data-content]") if (n.attributes.get("data-content") or "").endswith(" license")), None)
150 185 spdx = node.attributes["data-content"].removesuffix(" license").strip() if node else None
151 − facts.claim(ref, "license", spdx if spdx and spdx.upper() != "NOASSERTION" else None)
186 + claim_license(facts, ref, spdx if spdx and spdx.upper() != "NOASSERTION" else None)
152 187 facts.claim(ref, "license_name", lic.get("name") if isinstance(lic, dict) else None)
153 188 created = parse_datetime(repo_meta.get("createdAt")) if repo_meta.get("createdAt") else None
154 189 facts.claim(ref, "created_at", created.astimezone(UTC).isoformat(timespec="seconds") if created else None)
modified src/aiatlas/connectors/code/pypi.py +6 −5
@@ -20,7 +20,7 @@ from aiatlas.sdk.extract.dates import parse_datetime
20 20 from aiatlas.sdk.facts import EntityRef, Facts, Target
21 21 from aiatlas.sdk.fetch import FetchResult
22 22
23 −from .github import repo_entity, repo_entries
23 +from .github import claim_license, repo_entity, repo_entries
24 24
25 25 EXTRA_PACKAGES = [
26 26 "torch", "transformers", "vllm", "mlx", "mlx-lm", "llama-cpp-python", "langchain", "langgraph", "llama-index", "sglang", "jax", "onnxruntime",
@@ -42,8 +42,8 @@ class PyPIConnector(BaseConnector):
42 42 label = "PyPI — versions and metadata of the AI Python ecosystem"
43 43 description = "Public JSON documents of the packages in registry/repositories.yaml plus a curated list of standalone AI libraries."
44 44 source_key = "pypi.org"
45 − version = "1"
46 − parser_version = "1"
45 + version = "2"
46 + parser_version = "2"
47 47 interval_seconds = 12 * 3600
48 48 min_interval_seconds = 6 * 3600
49 49 max_interval_seconds = 3 * 86400
@@ -105,9 +105,10 @@ class PyPIConnector(BaseConnector):
105 105 facts.claim(ref, "latest_version", version)
106 106 facts.claim(ref, "latest_release_at", released)
107 107 lic = _license(info)
108 − facts.claim(ref, "pypi_license", lic)
108 + claim_license(facts, ref, lic, prop="pypi_license")
109 109 if not entry:
110 − facts.claim(ref, "license", lic) # mapped repos: GitHub's SPDX id owns `license` (avoids same-tier flip-flops)
110 + claim_license(facts, ref, lic) # mapped repos: GitHub's SPDX id owns `license` (avoids same-tier flip-flops)
111 + facts.claim(ref, "kind", "library")
111 112 summary = (info.get("summary") or "").strip()
112 113 facts.claim(ref, "description", summary[:1000] or None)
113 114 homepage = urls.get("homepage") or urls.get("home") or info.get("home_page")
modified src/aiatlas/connectors/hub/huggingface.py +228 −56
@@ -10,6 +10,14 @@ Sources (tier 2 for hub metadata — model-card statements are the authors' own)
10 10 language, quantized_by…) and the card text for later LLM passes. Not fetched for gated repositories (401 without auth).
11 11 * daily papers https://huggingface.co/papers → `DailyPapers.dailyPapers` → paper entities (arXiv id).
12 12
13 +Identity (ontology `aiatlas.ontology.models`): a repository is either the official checkpoint of a MODEL (`Qwen/Qwen3-8B` → model
14 +"Qwen3-8B", family hint "Qwen3") or an ARTIFACT of one — quantisation (`bartowski/Qwen3.8-27B-GGUF`, `zai-org/GLM-5-FP8`), conversion
15 +(`mlx-community/Kimi-K2.5-bf16`, ONNX/CoreML repacks) or packaging (a converter organisation re-uploading the same weights). Artifacts keep the
16 +full repo id as name and point to their canonical model through `EntityRef.canonical` (the `base_model` tag when present, otherwise the
17 +analysed base name with medium identity confidence); the writer materialises `canonical_id` / `artifact_of`. Effort labels, gating and
18 +licences are canonical: `license` is the ontology key (raw slug in `license_raw`), gating is `access: gated|open` (not an openness value),
19 +`weights_available: true` for every hub repository and `openness` derived from the ontology dimensions.
20 +
13 21 Discovery is bounded: `max_targets` (default 1500) and `models_per_org`; at 20 requests/minute a 1 100-target run takes ~55 minutes
14 22 (`aia run huggingface --max-targets 700` stays under 40 minutes).
15 23 """
@@ -21,6 +29,11 @@ from datetime import UTC, datetime
21 29 from typing import Any
22 30 from urllib.parse import quote
23 31
32 +from aiatlas.connectors._identity import family_ref
33 +from aiatlas.ontology.licenses import normalize_license
34 +from aiatlas.ontology.models import CONVERTER_ORGS, PRECISION_FORMATS, QUANT_FORMATS, NameAnalysis, analyze_model_name
35 +from aiatlas.ontology.openness import derive_openness, openness_dimensions
36 +from aiatlas.ontology.taxonomy import normalize_modalities
24 37 from aiatlas.registry import org_by_hf, org_ref, organizations
25 38 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
26 39 from aiatlas.sdk.extract.dates import parse_datetime
@@ -30,25 +43,43 @@ from aiatlas.sdk.fetch import FetchResult
30 43
31 44 HF = "https://huggingface.co"
32 45 PER_PAGE = 30
33 −QUANT_FORMATS = ("gguf", "mlx", "awq", "gptq", "fp8", "exl2", "exl3", "bitsandbytes", "onnx")
46 +QUANT_TAGS = ("gguf", "mlx", "awq", "gptq", "fp8", "exl2", "exl3", "bitsandbytes", "onnx", "compressed-tensors", "quantized")
34 47 GGUF_QUANT = re.compile(r"[-_.](IQ\d+_[A-Z0-9_]+|Q\d+_[A-Z0-9_]+|Q\d+|BF16|F16|F32|MXFP4)\.gguf$", re.IGNORECASE)
35 48 ORIGINAL_MODEL = re.compile(r"(?:Original|Base|Source) model:?\s*\[?(?:https?://huggingface\.co/)?([\w.-]+/[\w.-]+)", re.IGNORECASE)
36 49 BASE_MODEL_TAG = re.compile(r"^base_model:(?:(finetune|quantized|merge|adapter):)?([\w.-]+/[\w.-]+)$")
37 50 RELATION_BY_KIND = {"finetune": "fine_tuned_from", "quantized": "quantized_from", "merge": "merged_from", "adapter": "derived_from", None: "derived_from"}
38 51 NOISE_TAGS = {"endpoints_compatible", "text-generation-inference", "eval-results", "autotrain_compatible", "has_space", "conversational"}
39 −# Organizations that re-publish other labs' weights (quantizations, mirrors). Their repos keep the full `org/name` as entity name so that
40 −# `unsloth/Llama-3.1-8B-Instruct` never merges by alias into Meta's `Llama-3.1-8B-Instruct` (the resolver merges on normalized name).
52 +# Organizations that re-publish other labs' weights (quantizations, mirrors). Their repos are artifacts and keep the full `org/name` as
53 +# entity name so that `unsloth/Llama-3.1-8B-Instruct` never merges by alias into Meta's `Llama-3.1-8B-Instruct`.
41 54 REDISTRIBUTORS = {"bartowski", "unsloth", "mlx-community", "thebloke", "lmstudio-community", "quantfactory", "mradermacher", "nvidia-community",
42 − "ggml-org", "second-state", "turboderp", "casperhansen", "hugging-quants", "neuralmagic", "redhatai"}
55 + "ggml-org", "second-state", "turboderp", "casperhansen", "hugging-quants", "neuralmagic", "redhatai"} | CONVERTER_ORGS
43 56 # Model families and the hub organizations that publish them: `NousResearch/Meta-Llama-3.1-70B-Instruct` is a mirror of Meta's repo,
44 −# not Meta's entity, so it keeps its full id as name.
57 +# not Meta's entity, so it keeps its full id as name; the canonical model of a `bartowski/Qwen3.8-27B-GGUF` artifact belongs to Qwen.
45 58 FAMILY_ORGS = {r"^(meta-)?llama": {"meta-llama"}, r"^qwen|^qwq|^qvq": {"qwen"}, r"^deepseek": {"deepseek-ai"}, r"^(mistral|mixtral|magistral|devstral|codestral|ministral|pixtral|voxtral)": {"mistralai"},
46 59 r"^gemma|^paligemma|^shieldgemma|^medgemma": {"google"}, r"^phi-": {"microsoft"}, r"^glm|^chatglm|^cogview|^cogvideo": {"zai-org", "thudm"}, r"^kimi": {"moonshotai"},
47 60 r"^minimax": {"minimaxai"}, r"^gpt-oss": {"openai"}, r"^whisper": {"openai"}, r"^granite": {"ibm-granite"}, r"^(nvidia-)?nemotron": {"nvidia"},
48 61 r"^smollm|^smolvlm": {"huggingfacetb"}, r"^olmo|^molmo|^tulu": {"allenai"}, r"^(c4ai-)?command|^aya": {"coherelabs", "cohereforai"}, r"^flux": {"black-forest-labs"},
49 62 r"^stable-diffusion|^sdxl|^sd3": {"stabilityai"}, r"^claude": {"anthropic"}, r"^grok": {"xai-org"}, r"^seed-|^bagel": {"bytedance-seed"}, r"^hunyuan": {"tencent"},
50 63 r"^ernie": {"baidu"}, r"^jamba": {"ai21labs"}, r"^lfm": {"liquidai"}, r"^dbrx": {"databricks"}, r"^hermes": {"nousresearch"}}
51 −DERIVED_SUFFIX = re.compile(r"[-_.](gguf|mlx|awq|gptq|exl2|exl3|fp8|nvfp4|int4|int8|bnb[-_]4bit|4bit|8bit|mxfp4)(?:[-_.][\w.]+)?$", re.IGNORECASE)
64 +_QUANT_TOKEN = re.compile(r"^(w\d+a\d+|q\d(_[a-z0-9]+)*|iq\d(_[a-z0-9]+)*|\d-?bit|int\d|fp\d|nvfp\d|mxfp\d|ud-q\d.*|bnb-\d+bit)$", re.IGNORECASE)
65 +# pipeline tags without a "-to-" arrow → (input modalities, output modalities); `-to-` tags are split on the arrow
66 +PIPELINE_MODALITIES: dict[str, tuple[list[str], list[str]]] = {
67 + "text-generation": (["text"], ["text"]), "text2text-generation": (["text"], ["text"]), "fill-mask": (["text"], ["text"]), "translation": (["text"], ["text"]),
68 + "summarization": (["text"], ["text"]), "question-answering": (["text"], ["text"]), "conversational": (["text"], ["text"]),
69 + "text-classification": (["text"], ["structured"]), "token-classification": (["text"], ["structured"]), "zero-shot-classification": (["text"], ["structured"]),
70 + "feature-extraction": (["text"], ["embedding"]), "sentence-similarity": (["text"], ["embedding"]), "image-feature-extraction": (["image"], ["embedding"]),
71 + "automatic-speech-recognition": (["audio"], ["text"]), "audio-classification": (["audio"], ["structured"]), "text-to-speech": (["text"], ["audio"]),
72 + "text-to-audio": (["text"], ["audio"]), "audio-to-audio": (["audio"], ["audio"]), "voice-activity-detection": (["audio"], ["structured"]),
73 + "image-classification": (["image"], ["structured"]), "object-detection": (["image"], ["structured"]), "image-segmentation": (["image"], ["image"]),
74 + "zero-shot-image-classification": (["image", "text"], ["structured"]), "zero-shot-object-detection": (["image", "text"], ["structured"]),
75 + "depth-estimation": (["image"], ["image"]), "image-to-image": (["image"], ["image"]), "image-to-text": (["image"], ["text"]), "text-to-image": (["text"], ["image"]),
76 + "text-to-video": (["text"], ["video"]), "image-to-video": (["image"], ["video"]), "video-classification": (["video"], ["structured"]), "video-text-to-text": (["video", "text"], ["text"]),
77 + "image-text-to-text": (["image", "text"], ["text"]), "audio-text-to-text": (["audio", "text"], ["text"]), "visual-question-answering": (["image", "text"], ["text"]),
78 + "document-question-answering": (["document", "text"], ["text"]), "text-to-3d": (["text"], ["3d"]), "image-to-3d": (["image"], ["3d"]), "robotics": (["image", "text"], ["action"]),
79 + "reinforcement-learning": ([], ["action"]), "any-to-any": ([], []), "text-ranking": (["text"], ["structured"]), "mask-generation": (["image"], ["image"]),
80 + "keypoint-detection": (["image"], ["structured"]), "unconditional-image-generation": ([], ["image"]), "tabular-classification": (["structured"], ["structured"]),
81 + "tabular-regression": (["structured"], ["structured"]), "time-series-forecasting": (["structured"], ["structured"]), "graph-ml": (["structured"], ["structured"]),
82 +}
52 83
53 84
54 85 class HuggingFaceConnector(BaseConnector):
@@ -56,8 +87,8 @@ class HuggingFaceConnector(BaseConnector):
56 87 label = "Hugging Face Hub — models of the registry organizations, model cards, daily papers"
57 88 description = "Hub listing pages, model pages (embedded JSON), raw model cards and the daily-papers page — direct HTML, no API."
58 89 source_key = "huggingface.co"
59 − version = "1"
60 − parser_version = "1"
90 + version = "2"
91 + parser_version = "2"
61 92 interval_seconds = 6 * 3600
62 93 min_interval_seconds = 3 * 3600
63 94 max_interval_seconds = 2 * 86400
@@ -124,12 +155,12 @@ class HuggingFaceConnector(BaseConnector):
124 155 ref = self._model_ref(facts, repo_id, org)
125 156 facts.claim(ref, "hf_repo", repo_id)
126 157 facts.claim(ref, "model_card_url", f"{HF}/{repo_id}")
127 − facts.claim(ref, "pipeline_tag", m.get("pipeline_tag"))
158 + self._pipeline_claims(facts, ref, m.get("pipeline_tag"))
128 159 facts.claim(ref, "metric.downloads", m.get("downloads"))
129 160 facts.claim(ref, "metric.likes", m.get("likes"))
130 161 facts.claim(ref, "last_modified", _iso(m.get("lastModified")))
131 162 gated = m.get("gated")
132 − facts.claim(ref, "openness", "restricted" if gated else "open-weights")
163 + self._access_claims(facts, ref, gated)
133 164 if isinstance(m.get("numParameters"), int) and m["numParameters"] > 0:
134 165 facts.claim(ref, "parameter_count", m["numParameters"])
135 166 facts.follow(f"{HF}/{repo_id}", doc_type="model_page", entity=ref, key=f"model:{repo_id}", min_bytes=5000,
@@ -148,28 +179,34 @@ class HuggingFaceConnector(BaseConnector):
148 179 return
149 180 author = model.get("author") or repo_id.split("/")[0]
150 181 org = self._org_ref(facts, author, header.get("author"))
151 − ref = self._model_ref(facts, repo_id, org)
152 182 card = model.get("cardData") or {}
153 183 config = model.get("config") or {}
154 184 tags = [t for t in (model.get("tags") or []) if isinstance(t, str)]
155 185 gated = model.get("gated")
186 + bases = self._base_models(card, tags)
187 + quant_format = next((q for q in QUANT_TAGS if q in tags and q != "quantized"), None)
188 + if not quant_format and repo_id.lower().endswith(("-gguf", "_gguf")):
189 + quant_format = "gguf"
190 + if not quant_format and author.lower() == "mlx-community":
191 + quant_format = "mlx"
192 + ref = self._model_ref(facts, repo_id, org, bases=bases, quant_format=quant_format)
193 + is_artifact = ref.entity_type == "artifact"
156 194 facts.claim(ref, "hf_repo", repo_id)
157 195 facts.claim(ref, "model_card_url", f"{HF}/{repo_id}")
158 − facts.claim(ref, "pipeline_tag", model.get("pipeline_tag") or card.get("pipeline_tag"))
196 + self._pipeline_claims(facts, ref, model.get("pipeline_tag") or card.get("pipeline_tag"))
159 197 facts.claim(ref, "library_name", model.get("library_name") or card.get("library_name"))
160 198 license_ = card.get("license") or next((t.split(":", 1)[1] for t in tags if t.startswith("license:")), None)
161 199 if isinstance(license_, list):
162 200 license_ = license_[0] if license_ else None
163 − facts.claim(ref, "license", license_)
201 + license_key = self._license_claims(facts, ref, license_, card.get("license_name"))
164 202 facts.claim(ref, "license_url", card.get("license_link"))
165 − facts.claim(ref, "openness", "restricted" if gated else "open-weights")
166 − facts.claim(ref, "gated", str(gated) if gated else False)
203 + self._access_claims(facts, ref, gated, license_key=license_key)
167 204 facts.claim(ref, "release_date", _date(model.get("createdAt")))
168 205 facts.claim(ref, "last_modified", _iso(model.get("lastModified")))
169 206 facts.claim(ref, "metric.downloads", model.get("downloads"))
170 207 facts.claim(ref, "metric.downloads_all_time", model.get("downloadsAllTime"))
171 208 facts.claim(ref, "metric.likes", model.get("likes"))
172 − # parameters
209 + # parameters: the packaged size on artifacts, the safetensors count on official checkpoints (never on the canonical model of an artifact)
173 210 st = model.get("safetensors") or {}
174 211 total = st.get("total") if isinstance(st, dict) else None
175 212 listed = target.meta.get("num_parameters") # the hub's own count from the listing (GGUF repos have no safetensors)
@@ -196,25 +233,21 @@ class HuggingFaceConnector(BaseConnector):
196 233 paper = facts.entity("paper", f"arXiv:{t[6:]}", identifiers={"arxiv": t[6:]})
197 234 facts.relate(ref, "described_by", paper)
198 235 # quantization
199 − quant_format = next((q for q in QUANT_FORMATS if q in tags), None)
200 − if not quant_format and repo_id.lower().endswith(("-gguf", "_gguf")):
201 − quant_format = "gguf"
202 − if not quant_format and author.lower() == "mlx-community":
203 − quant_format = "mlx"
204 236 facts.claim(ref, "quant_format", quant_format)
205 237 quants = sorted({m.group(1).upper() for href, _ in html.links for m in [GGUF_QUANT.search(href)] if m and "/blob/main/" in href})
206 238 if not quants and quant_format == "mlx":
207 239 m = re.search(r"-(\d)bit\b", repo_id, re.IGNORECASE)
208 240 quants = [f"{m.group(1)}bit"] if m else []
209 241 facts.claim(ref, "quantization", quants)
210 − if quant_format:
242 + if quant_format or (is_artifact and ref.artifact_kind == "quantization"):
211 243 facts.claim(ref, "is_quantized", True)
212 − # base model relations
213 − for base_id, kind in self._base_models(card, tags):
214 − base = self._model_ref(facts, base_id, self._org_ref(facts, base_id.split("/")[0], None), develops=False)
215 − facts.claim(base, "hf_repo", base_id)
244 + if is_artifact:
245 + facts.claim(ref, "artifact_kind", ref.artifact_kind)
246 + # base model relations (artifact → model, fine-tune → base…)
247 + for base_id, kind in bases:
248 + base = self._base_ref(facts, base_id)
216 249 facts.relate(ref, RELATION_BY_KIND.get(kind, "derived_from"), base, attributes={"base_model_relation": kind} if kind else {})
217 − facts.claim(ref, "base_model", [b for b, _ in self._base_models(card, tags)] or None)
250 + facts.claim(ref, "base_model", [b for b, _ in bases] or None)
218 251 facts.claim(ref, "quantized_by", card.get("quantized_by"))
219 252 providers = [p.get("provider") for p in (model.get("availableInferenceProviders") or []) if isinstance(p, dict) and p.get("provider")]
220 253 facts.claim(ref, "hf_inference_providers", sorted(set(providers)))
@@ -234,33 +267,31 @@ class HuggingFaceConnector(BaseConnector):
234 267 repo_id = m.group(1) if m else None
235 268 if not repo_id:
236 269 return
270 + fm = md.front_matter or {}
271 + bases = [(b, None) for b in _listify(fm.get("base_model")) if "/" in b]
272 + quantized = bool(fm.get("quantized_by")) or "gguf" in [t.lower() for t in _listify(fm.get("tags"))]
273 + if not bases:
274 + m = ORIGINAL_MODEL.search(md.body)
275 + if m and m.group(1).lower() != repo_id.lower():
276 + bases = [(m.group(1), "quantized")]
277 + quantized = True
237 278 org = self._org_ref(facts, repo_id.split("/")[0], None)
238 − ref = target.entity or self._model_ref(facts, repo_id, org)
279 + ref = target.entity or self._model_ref(facts, repo_id, org, bases=bases, quant_format="gguf" if quantized else None)
239 280 if ref not in facts.entities:
240 281 facts.entities.append(ref)
241 − fm = md.front_matter or {}
242 282 lic = fm.get("license")
243 − facts.claim(ref, "license", lic[0] if isinstance(lic, list) and lic else (lic if isinstance(lic, str) else None))
244 − facts.claim(ref, "license_name", fm.get("license_name"))
245 − facts.claim(ref, "pipeline_tag", fm.get("pipeline_tag"))
283 + self._license_claims(facts, ref, lic[0] if isinstance(lic, list) and lic else (lic if isinstance(lic, str) else None), fm.get("license_name"))
284 + self._pipeline_claims(facts, ref, fm.get("pipeline_tag"))
246 285 facts.claim(ref, "library_name", fm.get("library_name"))
247 286 facts.claim(ref, "languages", _listify(fm.get("language")))
248 287 facts.claim(ref, "datasets", _listify(fm.get("datasets")))
249 288 facts.claim(ref, "quantized_by", fm.get("quantized_by"))
250 − bases = [b for b in _listify(fm.get("base_model")) if "/" in b]
251 − quantized = bool(fm.get("quantized_by")) or "gguf" in [t.lower() for t in _listify(fm.get("tags"))]
252 − if not bases:
253 − m = ORIGINAL_MODEL.search(md.body)
254 − if m and m.group(1).lower() != repo_id.lower():
255 − bases = [m.group(1)]
256 − quantized = True
257 − for base_id in bases:
258 − base = self._model_ref(facts, base_id, self._org_ref(facts, base_id.split("/")[0], None), develops=False)
259 − facts.claim(base, "hf_repo", base_id)
289 + for base_id, _kind in bases:
290 + base = self._base_ref(facts, base_id)
260 291 # the typed relation (finetune/quantized/merge/adapter) comes from the model page tags; the card only proves quantization
261 292 if quantized:
262 293 facts.relate(ref, "quantized_from", base)
263 − facts.claim(ref, "base_model", bases or None)
294 + facts.claim(ref, "base_model", [b for b, _ in bases] or None)
264 295 h1 = next((t for lvl, t in md.headings if lvl == 1), None)
265 296 facts.document_title = h1 or repo_id
266 297 facts.document_entity = ref
@@ -302,22 +333,120 @@ class HuggingFaceConnector(BaseConnector):
302 333 kind = "company" if (author_data or {}).get("type") == "org" else "organization"
303 334 ref = EntityRef(entity_type=kind, name=display, identifiers={"hf_org": hf_org}, aliases=[hf_org])
304 335 facts.claim(ref, "hf_org", hf_org)
305 − if ref not in facts.entities:
306 − facts.entities.append(ref)
336 + for e in facts.entities:
337 + if e.entity_type == ref.entity_type and e.identifiers and e.identifiers == ref.identifiers:
338 + return e
339 + facts.entities.append(ref)
340 + return ref
341 +
342 + def _model_ref(self, facts: Facts, repo_id: str, org: EntityRef, *, bases: list[tuple[str, str | None]] | None = None,
343 + quant_format: str | None = None) -> EntityRef:
344 + """Model or artifact EntityRef for a repository (one per repo inside a Facts)."""
345 + for e in facts.entities:
346 + if e.entity_type in ("model", "artifact") and e.identifiers.get("hf_repo") == repo_id:
347 + return e
348 + org_slug, _, repo_name = repo_id.partition("/")
349 + analysis = analyze_model_name(repo_id)
350 + kind = artifact_kind(repo_id, analysis, bases=bases or [], quant_format=quant_format)
351 + if kind:
352 + canonical = self._canonical_ref(facts, repo_id, analysis, bases or [])
353 + ref = facts.entity("artifact", repo_id, identifiers={"hf_repo": repo_id}, organization=org, artifact_kind=kind, canonical=canonical,
354 + identity_confidence="high" if any("/" in b for b, _ in (bases or [])) else "medium")
355 + facts.relate(ref, "published_by", org)
356 + return ref
357 + foreign = _foreign_family(org_slug, repo_name)
358 + name = repo_id if foreign else (repo_name or repo_id)
359 + # an artifact seen earlier in the document may already have created this model by name (`Qwen/Qwen3.8-27B-FP8` before
360 + # `Qwen/Qwen3.8-27B`): upgrade that ref in place instead of creating a second one
361 + existing = next((e for e in facts.entities if e.entity_type == "model" and not e.identifiers and e.name.lower() == name.lower()
362 + and (e.organization is None or e.organization.identifiers == org.identifiers)), None)
363 + if existing is not None and not foreign:
364 + existing.identifiers["hf_repo"] = repo_id
365 + existing.organization = org
366 + existing.aliases = [a for a in dict.fromkeys([*existing.aliases, repo_id]) if a != name]
367 + existing.family = existing.family or family_ref(repo_name, org)
368 + existing.identity_confidence = "high"
369 + facts.relate(org, "develops", existing)
370 + return existing
371 + ref = facts.entity("model", name, identifiers={"hf_repo": repo_id}, organization=org, aliases=[repo_id] if not foreign else [],
372 + family=family_ref(repo_name, org), identity_confidence="medium" if foreign else "high")
373 + facts.relate(org, "develops", ref)
307 374 return ref
308 375
309 − def _model_ref(self, facts: Facts, repo_id: str, org: EntityRef, *, develops: bool = True) -> EntityRef:
376 + def _base_ref(self, facts: Facts, base_id: str) -> EntityRef:
377 + """The model behind a `base_model` tag: an official repository → model ref (no `develops` here; its own page states it)."""
310 378 for e in facts.entities:
311 − if e.entity_type == "model" and e.identifiers.get("hf_repo") == repo_id:
379 + if e.entity_type in ("model", "artifact") and e.identifiers.get("hf_repo") == base_id:
312 380 return e
381 + base_org_slug, _, base_name = base_id.partition("/")
382 + base_org = self._org_ref(facts, base_org_slug, None)
383 + analysis = analyze_model_name(base_id)
384 + if artifact_kind(base_id, analysis, bases=[], quant_format=None):
385 + # a quantisation of a quantisation: the base is itself an artifact of the analysed model
386 + ref = facts.entity("artifact", base_id, identifiers={"hf_repo": base_id}, organization=base_org,
387 + artifact_kind=artifact_kind(base_id, analysis, bases=[], quant_format=None),
388 + canonical=self._canonical_ref(facts, base_id, analysis, []), identity_confidence="medium")
389 + else:
390 + foreign = _foreign_family(base_org_slug, base_name)
391 + ref = facts.entity("model", base_id if foreign else base_name, identifiers={"hf_repo": base_id}, organization=base_org,
392 + aliases=[base_id] if not foreign else [], family=family_ref(base_name, base_org), identity_confidence="high")
393 + facts.claim(ref, "hf_repo", base_id)
394 + return ref
395 +
396 + def _canonical_ref(self, facts: Facts, repo_id: str, analysis: NameAnalysis, bases: list[tuple[str, str | None]]) -> EntityRef:
397 + """The model an artifact packages: the `base_model` repo when present, otherwise the analysed base name (medium confidence)."""
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)
399 + if base and base.lower() != repo_id.lower():
400 + return self._base_ref(facts, base)
313 401 org_slug, _, repo_name = repo_id.partition("/")
314 − derived = org_slug.lower() in REDISTRIBUTORS or bool(DERIVED_SUFFIX.search(repo_name)) or _foreign_family(org_slug, repo_name)
315 − name = repo_id if derived else (repo_name or repo_id)
316 − ref = facts.entity("model", name, identifiers={"hf_repo": repo_id}, organization=org, aliases=[repo_id] if not derived else [])
317 − if develops:
318 − facts.relate(org, "develops", ref)
402 + name = canonical_name(repo_name)
403 + org: EntityRef | None = None
404 + family_orgs = _family_orgs(repo_name)
405 + if family_orgs and org_slug.lower() in family_orgs:
406 + org = self._org_ref(facts, org_slug, None)
407 + elif family_orgs:
408 + 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]
410 + elif org_slug.lower() not in REDISTRIBUTORS:
411 + org = self._org_ref(facts, org_slug, None)
412 + 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):
414 + return e
415 + ref = facts.entity("model", name, organization=org, family=family_ref(name, org), identity_confidence="medium")
319 416 return ref
320 417
418 + @staticmethod
419 + def _pipeline_claims(facts: Facts, ref: EntityRef, tag: Any) -> None:
420 + if not isinstance(tag, str) or not tag:
421 + return
422 + facts.claim(ref, "pipeline_tag", tag)
423 + mods_in, mods_out = pipeline_modalities(tag)
424 + facts.claim(ref, "modalities_input", mods_in)
425 + facts.claim(ref, "modalities_output", mods_out)
426 + facts.claim(ref, "modalities", sorted(set(mods_in) | set(mods_out)))
427 +
428 + @staticmethod
429 + def _license_claims(facts: Facts, ref: EntityRef, raw: str | None, license_name: Any = None) -> str | None:
430 + if not raw:
431 + return None
432 + key = normalize_license(raw)
433 + if (key is None or key == "Other") and isinstance(license_name, str) and license_name.strip():
434 + key = normalize_license(license_name) or key
435 + facts.claim(ref, "license_name", license_name.strip())
436 + facts.claim(ref, "license_raw", raw)
437 + facts.claim(ref, "license", key or raw)
438 + return key
439 +
440 + @staticmethod
441 + def _access_claims(facts: Facts, ref: EntityRef, gated: Any, *, license_key: str | None = None) -> None:
442 + facts.claim(ref, "access", "gated" if gated else "open")
443 + if isinstance(gated, str) and gated:
444 + facts.claim(ref, "gated_mode", gated)
445 + facts.claim(ref, "weights_available", True)
446 + if license_key is not None: # the category needs the licence terms; weights alone only prove `weights_available`
447 + dims = openness_dimensions(weights_available=True, license_key=license_key)
448 + facts.claim(ref, "openness", derive_openness(dims, license_key=license_key))
449 +
321 450 @staticmethod
322 451 def _base_models(card: dict[str, Any], tags: list[str]) -> list[tuple[str, str | None]]:
323 452 kinds: dict[str, str | None] = {}
@@ -331,13 +460,56 @@ class HuggingFaceConnector(BaseConnector):
331 460 return list(kinds.items())
332 461
333 462
334 −def _foreign_family(org_slug: str, repo_name: str) -> bool:
335 − """True when the repo name belongs to a well-known model family published by another hub organization (mirror / re-upload)."""
463 +# ---------------------------------------------------------------------------------------------- identity helpers (pure)
464 +def artifact_kind(repo_id: str, analysis: NameAnalysis | None = None, *, bases: list[tuple[str, str | None]], quant_format: str | None) -> str | None:
465 + """quantization | conversion | packaging for a repository that is not the official checkpoint of a model, else None."""
466 + a = analysis or analyze_model_name(repo_id)
467 + org_slug = repo_id.split("/")[0].lower() if "/" in repo_id else ""
468 + if a.is_quantized or quant_format or any(k == "quantized" for _, k in bases):
469 + return "quantization"
470 + if a.quant_formats or a.precision:
471 + return "conversion"
472 + if org_slug in REDISTRIBUTORS:
473 + return "packaging"
474 + return None
475 +
476 +
477 +def canonical_name(repo_name: str) -> str:
478 + """Repository name without quantisation / precision / packaging tokens, original casing kept: 'Qwen3.8-27B-GGUF' → 'Qwen3.8-27B',
479 + 'Llama-3.1-8B-Instruct-bnb-4bit' → 'Llama-3.1-8B-Instruct', 'Kimi-K2.5-bf16' → 'Kimi-K2.5'."""
480 + kept: list[str] = []
481 + for tok in repo_name.split("-"):
482 + low = tok.lower()
483 + if not tok or low in QUANT_FORMATS or low in PRECISION_FORMATS or _QUANT_TOKEN.match(low) or low == "ud" or re.match(r"^i?q\d", low):
484 + continue
485 + kept.append(tok)
486 + return "-".join(kept).strip("-") or repo_name
487 +
488 +
489 +def pipeline_modalities(tag: str) -> tuple[list[str], list[str]]:
490 + """Hub pipeline tag → (input modalities, output modalities), canonical vocabulary."""
491 + low = tag.strip().lower()
492 + if low in PIPELINE_MODALITIES:
493 + i, o = PIPELINE_MODALITIES[low]
494 + return normalize_modalities(i), normalize_modalities(o)
495 + if "-to-" in low:
496 + left, _, right = low.partition("-to-")
497 + return normalize_modalities(left.split("-")), normalize_modalities(right.split("-"))
498 + return [], []
499 +
500 +
501 +def _family_orgs(repo_name: str) -> set[str] | None:
336 502 low = repo_name.lower()
337 503 for pattern, orgs in FAMILY_ORGS.items():
338 504 if re.search(pattern, low):
339 − return org_slug.lower() not in orgs
340 − return False
505 + return {o.lower() for o in orgs}
506 + return None
507 +
508 +
509 +def _foreign_family(org_slug: str, repo_name: str) -> bool:
510 + """True when the repo name belongs to a well-known model family published by another hub organization (mirror / re-upload)."""
511 + orgs = _family_orgs(repo_name)
512 + return org_slug.lower() not in orgs if orgs else False
341 513
342 514
343 515 def _listify(v: Any) -> list[str]:
modified src/aiatlas/connectors/labs/_common.py +108 −3
@@ -5,6 +5,10 @@ import re
5 5 from datetime import datetime
6 6 from typing import Any
7 7
8 +from aiatlas.connectors._identity import family_ref
9 +from aiatlas.ontology.licenses import normalize_license
10 +from aiatlas.ontology.openness import derive_openness, openness_dimensions
11 +from aiatlas.ontology.taxonomy import normalize_modalities, normalize_status
8 12 from aiatlas.sdk.extract.dates import parse_datetime
9 13 from aiatlas.sdk.extract.feeds import FeedItem
10 14 from aiatlas.sdk.facts import EntityRef, Facts, Target
@@ -133,17 +137,95 @@ def month_year(cell: str) -> str | None:
133 137
134 138
135 139 def model_ref(facts: Facts, name: str, org: EntityRef, *, api_id: str | None = None, provider_key: str | None = None, family: str | None = None,
136 − aliases: list[str] | None = None) -> EntityRef:
137 − ids: dict[str, str] = {}
140 + aliases: list[str] | None = None, identifiers: dict[str, str] | None = None) -> EntityRef:
141 + """Official model EntityRef (identity = the lab's API id when given). `family` = the lab's own family label ("Claude", "Gemini") — when
142 + absent the ontology infers the versioned family ("Llama 3.1", "Qwen3") from the name; both become a `model_family` hint."""
143 + ids: dict[str, str] = dict(identifiers or {})
138 144 if api_id and provider_key:
139 145 ids[f"{provider_key}_model_id"] = api_id
140 − ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=aliases or [])
146 + fam = EntityRef(entity_type="model_family", name=family, organization=org, identifiers={"family_key": f"{re.sub(r'[^a-z0-9.]+', '-', family.lower()).strip('-')}@{org.slug_hint}"}) if family else family_ref(name, org)
147 + ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=aliases or [], family=fam, identity_confidence="high" if ids else "medium")
141 148 facts.relate(org, "develops", ref)
142 149 if family:
143 150 facts.claim(ref, "family", family)
151 + elif fam is not None:
152 + facts.claim(ref, "family", fam.name)
144 153 return ref
145 154
146 155
156 +# ---------------------------------------------------------------------------------------------- canonical vocabularies for lab docs
157 +# source label (lower-cased, punctuation-insensitive) → canonical capability slug
158 +CAPABILITY_MAP: dict[str, str] = {
159 + "function calling": "function_calling", "function_calling": "function_calling", "tool calling": "function_calling", "tool_calling": "function_calling",
160 + "tool use": "function_calling", "tool_use": "function_calling", "tools": "function_calling", "native tool use": "function_calling",
161 + "structured outputs": "structured_output", "structured_outputs": "structured_output", "structured output": "structured_output", "json mode": "structured_output",
162 + "json_mode": "structured_output", "json output": "structured_output", "response_format": "structured_output",
163 + "reasoning": "reasoning", "thinking": "reasoning", "extended thinking": "reasoning", "extended_thinking": "reasoning", "adaptive thinking": "reasoning",
164 + "vision": "vision", "image understanding": "vision", "image input": "vision", "image_input": "vision", "image inputs": "vision",
165 + "audio understanding": "audio_input", "audio input": "audio_input", "audio_input": "audio_input", "speech input": "audio_input",
166 + "audio generation": "audio_output", "audio output": "audio_output", "audio_output": "audio_output", "speech generation": "audio_output", "text to speech": "audio_output",
167 + "image generation": "image_generation", "image_generation": "image_generation", "video generation": "video_generation", "video_generation": "video_generation",
168 + "code execution": "code_execution", "code_execution": "code_execution", "code interpreter": "code_execution", "code_interpreter": "code_execution",
169 + "grounding with google search": "search_grounding", "search grounding": "search_grounding", "search_grounding": "search_grounding", "web search": "search_grounding",
170 + "web_search": "search_grounding", "grounding with google maps": "maps_grounding",
171 + "caching": "caching", "prompt caching": "caching", "prompt_caching": "caching", "context caching": "caching", "context_caching": "caching",
172 + "batch api": "batch", "batch": "batch", "batch_api": "batch", "batch mode": "batch",
173 + "fine tuning": "fine_tuning", "fine-tuning": "fine_tuning", "fine_tuning": "fine_tuning", "tuning": "fine_tuning", "supervised fine-tuning": "fine_tuning",
174 + "streaming": "streaming", "live api": "live_api", "live_api": "live_api", "realtime": "realtime", "computer use": "computer_use", "computer_use": "computer_use",
175 + "file search": "file_search", "file_search": "file_search", "url context": "url_context", "url_context": "url_context", "mcp": "mcp", "distillation": "distillation",
176 + "predicted outputs": "predicted_outputs", "predicted_outputs": "predicted_outputs", "embeddings": "embeddings", "flex inference": "flex_inference",
177 + "priority inference": "priority_inference", "citations": "citations", "pdf support": "document_input", "document understanding": "document_input", "files api": "document_input",
178 + "web fetch": "web_fetch", "memory": "memory", "agent skills": "agent_skills", "evals": "evals",
179 +}
180 +
181 +
182 +def normalize_capability(raw: str) -> str:
183 + s = re.sub(r"\s+", " ", str(raw).strip().lower())
184 + return CAPABILITY_MAP.get(s) or CAPABILITY_MAP.get(s.replace("_", " ")) or CAPABILITY_MAP.get(s.replace("-", " ")) or re.sub(r"[^a-z0-9]+", "_", s).strip("_")
185 +
186 +
187 +def normalize_capabilities(values: list[str] | None) -> list[str]:
188 + """Source labels / slugs → sorted canonical capability slugs (unknown labels are kept as slugs, never dropped)."""
189 + if not values:
190 + return []
191 + return sorted({normalize_capability(v) for v in values if str(v).strip()})
192 +
193 +
194 +def claim_license(facts: Facts, ref: EntityRef, raw: str | None, *, weights_available: bool | None = None) -> str | None:
195 + """`license` = ontology key (raw label kept in `license_raw`); with `weights_available` the openness category is derived, never asserted."""
196 + if not raw:
197 + return None
198 + key = normalize_license(raw)
199 + facts.claim(ref, "license_raw", raw)
200 + facts.claim(ref, "license", key or raw)
201 + if weights_available is not None:
202 + if weights_available:
203 + facts.claim(ref, "weights_available", True)
204 + dims = openness_dimensions(weights_available=weights_available, license_key=key)
205 + facts.claim(ref, "openness", derive_openness(dims, license_key=key))
206 + return key
207 +
208 +
209 +def claim_status(facts: Facts, ref: EntityRef, raw: str | None) -> str | None:
210 + if not raw:
211 + return None
212 + canon = normalize_status(raw)
213 + facts.claim(ref, "status", canon or raw)
214 + if canon and canon != str(raw).strip().lower():
215 + facts.claim(ref, "status_raw", raw)
216 + return canon
217 +
218 +
219 +def claim_api_aliases(facts: Facts, ref: EntityRef, aliases: list[str] | str | None) -> None:
220 + """`api_aliases` is always a list; `api_alias` (the first one) is kept for compatibility."""
221 + items = [aliases] if isinstance(aliases, str) else list(aliases or [])
222 + items = [a.strip() for a in items if isinstance(a, str) and a.strip()]
223 + if not items:
224 + return
225 + facts.claim(ref, "api_aliases", sorted(dict.fromkeys(items)))
226 + facts.claim(ref, "api_alias", items[0])
227 +
228 +
147 229 def first_target_with(targets: list[Target], key: str) -> Target | None:
148 230 return next((t for t in targets if t.key == key), None)
149 231
@@ -218,10 +300,31 @@ def slug_of(url: str) -> str:
218 300 return re.sub(r"\.(md|mdx|html?)$", "", seg)
219 301
220 302
303 +def claim_modalities(facts: Facts, ref: EntityRef, inputs: Any = None, outputs: Any = None) -> tuple[list[str], list[str]]:
304 + """Canonical modality lists (`pdf` → document, `Images` → image); `modalities` = union. Only what the page states."""
305 + mi, mo = normalize_modalities(inputs), normalize_modalities(outputs)
306 + if mi:
307 + facts.claim(ref, "modalities_input", mi)
308 + if mo:
309 + facts.claim(ref, "modalities_output", mo)
310 + if mi or mo:
311 + facts.claim(ref, "modalities", sorted(set(mi) | set(mo)))
312 + if "image" in mi:
313 + facts.claim(ref, "vision", True)
314 + if "audio" in mi:
315 + facts.claim(ref, "audio", True)
316 + return mi, mo
317 +
318 +
221 319 __all__ = [
320 + "CAPABILITY_MAP",
222 321 "MODEL_WORDS",
223 322 "RELEASE_WORDS",
224 323 "announcement_events",
324 + "claim_api_aliases",
325 + "claim_license",
326 + "claim_modalities",
327 + "claim_status",
225 328 "clean_cell",
226 329 "first_target_with",
227 330 "iso_date",
@@ -232,6 +335,8 @@ __all__ = [
232 335 "money",
233 336 "month_year",
234 337 "next_flight_payload",
338 + "normalize_capabilities",
339 + "normalize_capability",
235 340 "parse_retirement",
236 341 "slug_of",
237 342 "tokens",
modified src/aiatlas/connectors/labs/anthropic.py +39 −9
@@ -20,12 +20,16 @@ from aiatlas.sdk.fetch import FetchResult
20 20
21 21 from ._common import (
22 22 announcement_events,
23 + claim_api_aliases,
24 + claim_modalities,
25 + claim_status,
23 26 clean_cell,
24 27 kv_tables,
25 28 link_in_cell,
26 29 model_ref,
27 30 money,
28 31 month_year,
32 + normalize_capabilities,
29 33 parse_retirement,
30 34 tokens,
31 35 transpose_feature_table,
@@ -94,10 +98,10 @@ class AnthropicConnector(BaseConnector):
94 98 aliases = [a for a in (api_id, alias) if a]
95 99 ref = model_ref(facts, model_name, org, api_id=api_id, provider_key=PROVIDER_KEY, family="Claude", aliases=aliases + _name_variants(model_name))
96 100 facts.claim(ref, "openness", "proprietary")
97 − facts.claim(ref, "status", "active")
101 + claim_status(facts, ref, "active")
98 102 facts.claim(ref, "description", feats.get("description"))
99 103 facts.claim(ref, "api_model_id", api_id)
100 − facts.claim(ref, "api_alias", alias)
104 + claim_api_aliases(facts, ref, alias)
101 105 facts.claim(ref, "context_length", tokens(feats.get("context window", "")), unit="tokens")
102 106 facts.claim(ref, "max_output_tokens", tokens(feats.get("max output", "")), unit="tokens")
103 107 facts.claim(ref, "knowledge_cutoff", month_year(feats.get("reliable knowledge cutoff", "")))
@@ -105,11 +109,8 @@ class AnthropicConnector(BaseConnector):
105 109 facts.claim(ref, "latency_tier", feats.get("comparative latency"))
106 110 facts.claim(ref, "thinking", feats.get("thinking") or feats.get("extended thinking"))
107 111 facts.claim(ref, "default_effort", feats.get("default effort"))
108 − facts.claim(ref, "modalities", ["text", "image"])
109 − facts.claim(ref, "modalities_input", ["text", "image"])
110 − facts.claim(ref, "modalities_output", ["text"])
111 − facts.claim(ref, "tool_calling", True)
112 − facts.claim(ref, "vision", True)
112 + # modalities / capabilities only when the comparison table has a row for them (no hard-coded constants)
113 + self._table_capabilities(facts, ref, feats)
113 114 retire, tentative = parse_retirement(feats.get("retirement", ""))
114 115 if retire:
115 116 facts.claim(ref, "retirement_date", retire)
@@ -140,6 +141,35 @@ class AnthropicConnector(BaseConnector):
140 141 ref = next((e for e in facts.entities if e.entity_type == "model" and e.name == name), None)
141 142 facts.follow(url.rstrip("/") + ".md", doc_type="model_page", entity=ref, key=f"model:{name}", meta={"model": name}, min_bytes=500)
142 143
144 + @staticmethod
145 + def _table_capabilities(facts: Facts, ref, feats: dict[str, str]) -> None: # type: ignore[no-untyped-def]
146 + """Rows of the comparison table that state modalities or capabilities → canonical claims; absent rows → no claim."""
147 + caps: list[str] = []
148 + yes = re.compile(r"^(yes|supported|✓|✔|adaptive|adaptive \(always on\)|extended|available)", re.IGNORECASE)
149 + no = re.compile(r"^(no|not supported|—|-|n/a|unsupported)$", re.IGNORECASE)
150 + for key, cap in (("thinking", "reasoning"), ("extended thinking", "reasoning"), ("vision", "vision"), ("image input", "vision"), ("tool use", "function_calling"),
151 + ("function calling", "function_calling"), ("structured outputs", "structured_output"), ("prompt caching", "caching"), ("batch api", "batch"),
152 + ("computer use", "computer_use"), ("pdf support", "document_input"), ("citations", "citations"), ("web search", "search_grounding")):
153 + v = feats.get(key)
154 + if v is None or v == "":
155 + continue
156 + if no.match(v.strip()):
157 + continue
158 + if yes.match(v.strip()) or key in ("thinking", "extended thinking"):
159 + caps.append(cap)
160 + if caps:
161 + facts.claim(ref, "capabilities", normalize_capabilities(caps))
162 + if "function_calling" in caps:
163 + facts.claim(ref, "tool_calling", True)
164 + if "vision" in caps:
165 + facts.claim(ref, "vision", True)
166 + if "reasoning" in caps:
167 + facts.claim(ref, "reasoning", True)
168 + mods = feats.get("modalities") or feats.get("input modalities") or feats.get("input")
169 + outs = feats.get("output modalities") or feats.get("output")
170 + if mods or outs:
171 + claim_modalities(facts, ref, mods, outs)
172 +
143 173 # ------------------------------------------------------------------------------------------ model page
144 174 def _model_page(self, facts: Facts, org, target: Target, parsed: Parsed) -> None: # type: ignore[no-untyped-def]
145 175 md = parsed.markdown
@@ -205,7 +235,7 @@ class AnthropicConnector(BaseConnector):
205 235 ref = model_ref(facts, model_name, org, family="Claude", aliases=_name_variants(model_name))
206 236 facts.claim(ref, "openness", "proprietary")
207 237 if status:
208 − facts.claim(ref, "status", status)
238 + claim_status(facts, ref, status)
209 239 facts.claim(ref, "availability_note", note)
210 240 facts.price(model=ref, provider=provider, input_per_mtok=money(row[c_in]), output_per_mtok=money(row[c_out]),
211 241 cached_input_per_mtok=money(row[c_hit]) if c_hit is not None and c_hit < len(row) else None,
@@ -240,7 +270,7 @@ class AnthropicConnector(BaseConnector):
240 270 display = _display_name(api_id)
241 271 ref = model_ref(facts, display, org, api_id=api_id, provider_key=PROVIDER_KEY, family="Claude", aliases=[api_id] + _name_variants(display))
242 272 state = clean_cell(row[i_state]).lower()
243 − facts.claim(ref, "status", {"active": "active", "retired": "retired", "deprecated": "deprecated"}.get(state, state))
273 + claim_status(facts, ref, state)
244 274 dep = clean_cell(row[i_dep])
245 275 if dep and dep.upper() != "N/A":
246 276 d = parse_datetime(dep)
modified src/aiatlas/connectors/labs/cohere.py +21 −8
@@ -10,6 +10,8 @@ from __future__ import annotations
10 10 import re
11 11 from typing import Any
12 12
13 +from aiatlas.ontology.openness import normalize_openness
14 +from aiatlas.ontology.taxonomy import normalize_modalities
13 15 from aiatlas.registry import org_ref, provider_ref
14 16 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
15 17 from aiatlas.sdk.extract.dates import parse_datetime
@@ -18,7 +20,15 @@ from aiatlas.sdk.extract.numbers import parse_active_params, parse_param_count
18 20 from aiatlas.sdk.facts import EntityRef, Facts, Target
19 21 from aiatlas.sdk.fetch import FetchResult
20 22
21 −from ._common import announcement_events, clean_cell, model_ref, next_flight_payload, tokens
23 +from ._common import (
24 + announcement_events,
25 + claim_api_aliases,
26 + claim_status,
27 + clean_cell,
28 + model_ref,
29 + next_flight_payload,
30 + tokens,
31 +)
22 32
23 33 DOCS = "https://docs.cohere.com/docs/models"
24 34 BLOG = "https://cohere.com/blog"
@@ -114,7 +124,9 @@ class CohereConnector(BaseConnector):
114 124 ref = _model(facts, org, target_id, None)
115 125 if alias not in ref.aliases:
116 126 ref.aliases.append(alias)
117 − facts.claim(ref, "api_alias", alias)
127 + existing = next((c.value for c in facts.claims if c.entity is ref and c.property == "api_aliases"), [])
128 + facts.claims = [c for c in facts.claims if not (c.entity is ref and c.property in ("api_aliases", "api_alias"))]
129 + claim_api_aliases(facts, ref, [*existing, alias])
118 130 status, dep = _status(row.get("status", ""))
119 131 if status == "deprecated" and dep:
120 132 facts.claim(ref, "alias_deprecation_date", dep)
@@ -122,10 +134,10 @@ class CohereConnector(BaseConnector):
122 134 def _row_claims(self, facts: Facts, ref: EntityRef, row: dict[str, str], desc: str) -> None:
123 135 facts.claim(ref, "description", desc or None)
124 136 status, dep = _status(row.get("status", ""))
125 − facts.claim(ref, "status", status)
137 + claim_status(facts, ref, status)
126 138 facts.claim(ref, "deprecation_date", dep)
127 139 mods_raw = row.get("modality") or row.get("modalities") or ""
128 − mods = _modalities(mods_raw)
140 + mods = normalize_modalities(_modalities(mods_raw))
129 141 if mods:
130 142 facts.claim(ref, "modalities", mods)
131 143 facts.claim(ref, "modalities_input", mods)
@@ -145,10 +157,11 @@ class CohereConnector(BaseConnector):
145 157 low = desc.lower()
146 158 if re.search(r"\bMoE\b|mixture[- ]of[- ]experts", desc, re.IGNORECASE):
147 159 facts.claim(ref, "is_moe", True)
148 − if "open-weight" in low or "open weight" in low:
149 − facts.claim(ref, "openness", "open-weights")
150 − elif "open source" in low or "open-source" in low:
151 − facts.claim(ref, "openness", "open-source")
160 + if "open-weight" in low or "open weight" in low or "open source" in low or "open-source" in low:
161 + # the docs prose says "open"; the weights exist on the hub (Cohere Labs) — the category itself is derived from the licence elsewhere
162 + facts.claim(ref, "weights_available", True)
163 + facts.claim(ref, "openness", normalize_openness("open-weights"))
164 + facts.claim(ref, "openness_raw", "open-source" if "open source" in low or "open-source" in low else "open-weights")
152 165 if re.search(r"\d+(?:\.\d+)?B\s+(total|parameter|params|instruct|model)", desc):
153 166 facts.claim(ref, "parameter_count", parse_param_count(desc))
154 167 active = parse_active_params(desc)
modified src/aiatlas/connectors/labs/google.py +14 −14
@@ -28,10 +28,14 @@ from ._common import (
28 28 MODEL_WORDS,
29 29 RELEASE_WORDS,
30 30 announcement_events,
31 + claim_api_aliases,
32 + claim_modalities,
33 + claim_status,
31 34 clean_cell,
32 35 model_ref,
33 36 money,
34 37 month_year,
38 + normalize_capabilities,
35 39 slug_of,
36 40 tokens,
37 41 )
@@ -121,7 +125,7 @@ class GoogleConnector(BaseConnector):
121 125 ref = _model(facts, org, name, api_id)
122 126 desc = card.css_first("p.description-centered")
123 127 facts.claim(ref, "description", node_text(desc) if desc else None)
124 − facts.claim(ref, "status", status)
128 + claim_status(facts, ref, status)
125 129 if api_id not in ids:
126 130 ids.append(api_id)
127 131 api_aliases: dict[int, tuple[EntityRef, set[str]]] = {}
@@ -148,13 +152,13 @@ class GoogleConnector(BaseConnector):
148 152 api_aliases.setdefault(id(ref), (ref, set()))[1].update(extra)
149 153 note = re.search(r"\(([^)]*)\)", raw_name)
150 154 if note:
151 − facts.claim(ref, "status", _status(note.group(1)))
155 + claim_status(facts, ref, _status(note.group(1)))
152 156 if i_desc is not None and i_desc < len(r):
153 157 facts.claim(ref, "description", clean_cell(r[i_desc]))
154 158 if endpoints[0] not in ids:
155 159 ids.append(endpoints[0])
156 160 for ref, extra in api_aliases.values():
157 − facts.claim(ref, "api_aliases", sorted(extra))
161 + claim_api_aliases(facts, ref, sorted(extra))
158 162 for api_id in ids[:MAX_MODEL_PAGES]:
159 163 facts.follow(f"{DOCS}/models/{api_id}", doc_type="model_page", key=f"model:{api_id}", meta={"api_id": api_id}, min_bytes=3000, priority=1)
160 164
@@ -180,14 +184,7 @@ class GoogleConnector(BaseConnector):
180 184 types = props.get("supported data types", "")
181 185 m = re.search(r"Inputs?\s+(.*?)\s+Outputs?\s+(.*)$", types, re.IGNORECASE)
182 186 if m:
183 − mods_in, mods_out = _modalities(m.group(1)), _modalities(m.group(2))
184 − facts.claim(ref, "modalities_input", mods_in)
185 − facts.claim(ref, "modalities_output", mods_out)
186 − facts.claim(ref, "modalities", sorted(set(mods_in) | set(mods_out)))
187 − if "image" in mods_in:
188 − facts.claim(ref, "vision", True)
189 − if "audio" in mods_in:
190 − facts.claim(ref, "audio", True)
187 + claim_modalities(facts, ref, _modalities(m.group(1)), _modalities(m.group(2))) # pdf → document, images → image (ontology)
191 188 limits = props.get("token limits", "")
192 189 m_in = re.search(r"Input token limit\s+([\d,]+)", limits)
193 190 m_out = re.search(r"Output token limit\s+([\d,]+)", limits)
@@ -201,7 +198,9 @@ class GoogleConnector(BaseConnector):
201 198 for cap, state, detail in CAPABILITY.findall(caps):
202 199 supported[cap] = (detail or True) if state == "Supported" else False
203 200 if supported:
204 − facts.claim(ref, "capabilities", sorted(k for k, v in supported.items() if v))
201 + labels = sorted(k for k, v in supported.items() if v)
202 + facts.claim(ref, "capabilities", normalize_capabilities(labels))
203 + facts.claim(ref, "capabilities_raw", labels)
205 204 if "Function calling" in supported:
206 205 facts.claim(ref, "tool_calling", bool(supported["Function calling"]))
207 206 if "Structured outputs" in supported:
@@ -308,9 +307,9 @@ def _model(facts: Facts, org: EntityRef, name: str, api_id: str) -> EntityRef:
308 307 for e in facts.entities:
309 308 if e.entity_type == "model" and e.identifiers.get(ID_SCHEME) == api_id:
310 309 return e
311 − ref = model_ref(facts, name, org, api_id=api_id, provider_key="gemini", aliases=[api_id] if api_id != name else [])
310 + # `gemini_model_id` stays the primary scheme (existing rows resolve on it); `google_model_id` is emitted alongside for the provider key
311 + ref = model_ref(facts, name, org, api_id=api_id, provider_key="gemini", aliases=[api_id] if api_id != name else [], identifiers={"google_model_id": api_id})
312 312 facts.claim(ref, "api_model_id", api_id)
313 − facts.claim(ref, "family", "Gemini" if api_id.startswith("gemini") else None)
314 313 return ref
315 314
316 315
@@ -334,6 +333,7 @@ def _status(text: str) -> str | None:
334 333
335 334
336 335 def _modalities(text: str) -> list[str]:
336 + """Raw modality words of a 'Supported data types' cell (canonicalised by `claim_modalities`: pdf → document)."""
337 337 out: list[str] = []
338 338 for tok in re.split(r"[,/]|\band\b", text.lower()):
339 339 tok = tok.strip().strip(".")
modified src/aiatlas/connectors/labs/mistral.py +30 −11
@@ -19,6 +19,7 @@ from typing import Any
19 19
20 20 from selectolax.parser import HTMLParser
21 21
22 +from aiatlas.connectors._identity import family_ref
22 23 from aiatlas.registry import org_ref, provider_ref
23 24 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
24 25 from aiatlas.sdk.extract.dates import parse_datetime
@@ -26,7 +27,18 @@ from aiatlas.sdk.extract.html import node_text
26 27 from aiatlas.sdk.facts import EntityRef, Facts, Target
27 28 from aiatlas.sdk.fetch import FetchResult
28 29
29 −from ._common import announcement_events, json_after, model_ref, next_flight_payload, slug_of, tokens
30 +from ._common import (
31 + announcement_events,
32 + claim_api_aliases,
33 + claim_license,
34 + claim_status,
35 + json_after,
36 + model_ref,
37 + next_flight_payload,
38 + normalize_capabilities,
39 + slug_of,
40 + tokens,
41 +)
30 42
31 43 DOCS = "https://docs.mistral.ai"
32 44 NEWS_RSS = "https://mistral.ai/news/rss"
@@ -123,7 +135,7 @@ class MistralConnector(BaseConnector):
123 135 dep, ret = (dates + [None, None])[:2]
124 136 facts.claim(ref, "deprecation_date", dep)
125 137 facts.claim(ref, "retirement_date", ret)
126 − facts.claim(ref, "status", "retired" if ret and ret < observed.date().isoformat() else "deprecated")
138 + claim_status(facts, ref, "retired" if ret and ret < observed.date().isoformat() else "deprecated")
127 139 alt_link = tds[4].css_first("a[href]")
128 140 alt_name = node_text(tds[4]).strip()
129 141 if alt_name and alt_name != name:
@@ -153,14 +165,17 @@ class MistralConnector(BaseConnector):
153 165 m = LICENSE_SENTENCE.search(intro or "")
154 166 if m:
155 167 facts.claim(ref, "description", intro.split(" Released")[0].strip() if intro and " Released" in intro else None)
156 − facts.claim(ref, "license", m.group(1).strip())
157 − facts.claim(ref, "openness", "open-weights")
168 + claim_license(facts, ref, m.group(1).strip(), weights_available=True)
158 169 features_section = html.css_first("h3#features, h3[id*='features']")
159 170 feats = [node_text(n) for n in html.css("[class*='LinkItem_link'] span, [class*='LinkItem_link'] p")] if features_section is None else []
160 171 blob = " | ".join(feats) if feats else text
172 + caps: list[str] = []
161 173 for label, prop in (("Function Calling", "tool_calling"), ("Structured Outputs", "structured_output"), ("Vision", "vision"), ("Reasoning", "reasoning")):
162 174 if re.search(rf"\b{label}\b", blob):
163 175 facts.claim(ref, prop, True)
176 + caps.append(label)
177 + if caps:
178 + facts.claim(ref, "capabilities", normalize_capabilities(caps))
164 179 payload = next_flight_payload(res.content)
165 180 if payload:
166 181 pricing = json_after(payload, '"pricing":')
@@ -178,11 +193,11 @@ class MistralConnector(BaseConnector):
178 193 feats_d["eur_output_per_mtok"] = eur_out
179 194 facts.price(model=ref, provider=provider, input_per_mtok=p_in, output_per_mtok=p_out, features=feats_d, meta={"from": "model page"})
180 195 if re.search(r'"isRetired":true', payload):
181 − facts.claim(ref, "status", "retired")
196 + claim_status(facts, ref, "retired")
182 197 names = json_after(payload, '"names":')
183 198 if isinstance(names, list):
184 − latest = next((n for n in names if isinstance(n, str) and n.endswith("-latest")), None)
185 − facts.claim(ref, "api_alias", latest)
199 + latest = [n for n in names if isinstance(n, str) and n.endswith("-latest")]
200 + claim_api_aliases(facts, ref, latest)
186 201
187 202
188 203 # ---------------------------------------------------------------------------------------------- helpers
@@ -202,7 +217,8 @@ def _model(facts: Facts, org: EntityRef, name: str, slug: str | None, api_id: st
202 217 ids[SLUG_SCHEME] = slug
203 218 if api_id:
204 219 ids[ID_SCHEME] = api_id
205 − ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=[api_id] if api_id else [])
220 + ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=[api_id] if api_id else [], family=family_ref(name, org),
221 + identity_confidence="high" if ids else "medium")
206 222 if org not in facts.entities:
207 223 facts.entities.append(org)
208 224 facts.relate(org, "develops", ref)
@@ -238,16 +254,19 @@ def _third_party(name: str, description: str) -> EntityRef | None:
238 254
239 255
240 256 def _license_claims(facts: Facts, ref: EntityRef, badge: str) -> None:
257 + """Weights-licence badge ("Apache 2.0", "MRL", "Modified MIT", "Proprietary") → canonical `license` (+ `license_raw`) and the openness
258 + category derived from the ontology (MRL → restricted-weights, Apache 2.0 → open-weights)."""
241 259 badge = badge.strip()
242 260 if not badge:
243 261 return
244 262 low = badge.lower()
245 263 if OPEN_LICENSES.search(badge):
246 − facts.claim(ref, "license", badge)
247 − facts.claim(ref, "openness", "open-weights")
264 + claim_license(facts, ref, badge, weights_available=True)
248 265 elif "proprietary" in low or "commercial" in low or "api only" in low or "closed" in low:
249 266 facts.claim(ref, "openness", "proprietary")
250 − facts.claim(ref, "license", badge if "licen" in low else None)
267 + facts.claim(ref, "weights_available", False)
268 + if "licen" in low:
269 + claim_license(facts, ref, badge)
251 270
252 271
253 272 def _us_date(m: re.Match[str]) -> str | None:
modified src/aiatlas/connectors/labs/openai.py +16 −10
@@ -16,6 +16,7 @@ import re
16 16 from datetime import datetime
17 17 from typing import Any
18 18
19 +from aiatlas.ontology.taxonomy import normalize_modalities
19 20 from aiatlas.registry import org_ref, provider_ref
20 21 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
21 22 from aiatlas.sdk.extract.dates import parse_datetime
@@ -23,7 +24,16 @@ from aiatlas.sdk.extract.markdown import parse_markdown
23 24 from aiatlas.sdk.facts import EntityRef, Facts, Target
24 25 from aiatlas.sdk.fetch import FetchResult
25 26
26 −from ._common import announcement_events, clean_cell, model_ref, money, month_year, tokens
27 +from ._common import (
28 + announcement_events,
29 + claim_status,
30 + clean_cell,
31 + model_ref,
32 + money,
33 + month_year,
34 + normalize_capabilities,
35 + tokens,
36 +)
27 37
28 38 DOCS = "https://developers.openai.com/api/docs"
29 39 NEWS_RSS = "https://openai.com/news/rss.xml"
@@ -162,7 +172,8 @@ class OpenAIConnector(BaseConnector):
162 172 facts.claim(ref, "fine_tuning_available", "fine_tuning" in features)
163 173 if "reasoning" in features:
164 174 facts.claim(ref, "reasoning", True)
165 − facts.claim(ref, "capabilities", features)
175 + facts.claim(ref, "capabilities", normalize_capabilities(features)) # OpenAI slugs → canonical (structured_outputs → structured_output…)
176 + facts.claim(ref, "capabilities_raw", features)
166 177 snapshots = [clean_cell(ln.strip().lstrip("-").strip()) for ln in md.section(r"^Snapshots").split("\n") if ln.strip().startswith("-")]
167 178 for s in snapshots:
168 179 if s and API_ID.match(s) and s not in ref.aliases and s != api_id:
@@ -349,9 +360,9 @@ class OpenAIConnector(BaseConnector):
349 360 if latest["shutdown"]:
350 361 facts.claim(ref, "retirement_date", latest["shutdown"])
351 362 facts.claim(ref, "retirement_tentative", latest["tentative"])
352 − facts.claim(ref, "status", "retired" if latest["shutdown"] < today else "deprecated")
363 + claim_status(facts, ref, "retired" if latest["shutdown"] < today else "deprecated")
353 364 else:
354 − facts.claim(ref, "status", "deprecated")
365 + claim_status(facts, ref, "deprecated")
355 366 if entry["snapshots"]:
356 367 facts.claim(ref, "retired_snapshots", sorted(entry["snapshots"]))
357 368 if latest["replacement"] and latest["replacement"] != base:
@@ -375,12 +386,7 @@ def _model(facts: Facts, org: EntityRef, name: str, api_id: str, *, claim_id: bo
375 386
376 387
377 388 def _modalities(cell: str) -> list[str]:
378 − out = []
379 − for part in re.split(r"[,/]| and ", cell.lower()):
380 − part = part.strip()
381 − if part in ("text", "image", "audio", "video", "code", "embedding"):
382 − out.append(part)
383 − return out
389 + return normalize_modalities(cell)
384 390
385 391
386 392 def _split_model_cell(cell: str) -> tuple[str | None, str | None]:
modified src/aiatlas/connectors/providers/openrouter.py +38 −39
@@ -6,10 +6,13 @@ modalities, tokenizer), `pricing` (USD per token as strings → ×1e6 = USD per
6 6 `supported_parameters`, `created` (epoch), `hugging_face_id`. Entries whose id starts with `~` are alias rows (`alias_target`) and are
7 7 skipped; `-1` prices (dynamic routing) are skipped.
8 8
9 −Model identity: `{"openrouter": "<vendor/slug>"}` (variant suffix stripped) + `{"hf_repo": …}` when the catalogue gives it.
10 −Pricing: `PriceObs(provider=provider_ref(<key>))` where `<key>` = `provider_by_openrouter(vendor)` (registry `openrouter_slug`, plus a small
11 −vendor→provider alias map for `x-ai`/`mistralai`) when the vendor is itself a registered provider, otherwise the `openrouter` aggregator;
12 −`features["via"] = "openrouter"` and `provider_model_id` = the full OpenRouter id (variants become separate price rows).
9 +Model identity: `{"openrouter": "<vendor/slug>"}` (variant suffix stripped) + `{"hf_repo": …}` when the catalogue gives it; aliases and the
10 +developer 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.
12 +Pricing: every price is booked on the **OpenRouter provider entity only** (`provider_ref("openrouter")`) with `features.upstream_provider` =
13 +the registry provider key of the routed vendor when it is one — never on the lab's own provider entity, which would create a second
14 +"current" price per model × provider. `provider_model_id` = the full OpenRouter id (variants `:free` / `:thinking` / `:nitro` are separate
15 +price rows of the same model, `features.variant`).
13 16 """
14 17 from __future__ import annotations
15 18
@@ -17,18 +20,15 @@ import re
17 20 from datetime import UTC, datetime
18 21 from typing import Any
19 22
20 −from aiatlas.registry import org_by_hf, org_ref, organizations, provider_by_openrouter, provider_ref, providers
23 +from aiatlas.connectors._identity import family_ref, model_identity, org_key_for_vendor, org_ref_in
24 +from aiatlas.ontology.taxonomy import normalize_modalities
25 +from aiatlas.registry import provider_by_openrouter, provider_ref, providers
21 26 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
22 27 from aiatlas.sdk.facts import EntityRef, Facts, Target
23 28 from aiatlas.sdk.fetch import FetchResult
24 29
25 30 URL = "https://openrouter.ai/api/v1/models"
26 31 VENDOR_PROVIDER = {"x-ai": "xai", "mistralai": "mistral"}
27 −VENDOR_ORG = {"meta": "meta-ai", "meta-llama": "meta-ai", "liquid": "liquid-ai", "x-ai": "xai", "mistralai": "mistral", "z-ai": "zhipu",
28 − "moonshotai": "moonshot", "bytedance": "bytedance", "bytedance-seed": "bytedance", "ibm-granite": "ibm", "google": "google",
29 − "amazon": "amazon", "qwen": "qwen", "deepseek": "deepseek", "anthropic": "anthropic", "openai": "openai", "cohere": "cohere",
30 − "nvidia": "nvidia", "microsoft": "microsoft", "perplexity": "perplexity", "minimax": "minimax", "tencent": "tencent", "baidu": "baidu",
31 − "ai21": "ai21", "nousresearch": "nous-research", "openrouter": "openrouter"}
32 32 VARIANTS = {"free", "extended", "nitro", "floor", "online", "thinking", "beta", "exacto", "fast"}
33 33
34 34
@@ -37,8 +37,8 @@ class OpenRouterConnector(BaseConnector):
37 37 label = "OpenRouter — model catalogue with routed pricing"
38 38 description = "Public JSON catalogue: context, modalities, supported parameters and per-token prices of every model routed by OpenRouter."
39 39 source_key = "openrouter.ai"
40 − version = "1"
41 − parser_version = "1"
40 + version = "2"
41 + parser_version = "2"
42 42 interval_seconds = 3 * 3600
43 43 min_interval_seconds = 3600
44 44 max_interval_seconds = 86400
@@ -79,14 +79,16 @@ class OpenRouterConnector(BaseConnector):
79 79 variant = variant.lower() if variant else None
80 80 base_id = f"{vendor}/{base_slug}"
81 81 name = _display_name(item.get("name") or base_slug, vendor)
82 − org, provider_key = self._vendor(facts, vendor, item.get("name") or "")
82 + org, provider_key = self._vendor(facts, vendor, item.get("name") or "", base_slug)
83 83 ids = {"openrouter": base_id}
84 84 hf = item.get("hugging_face_id")
85 85 if isinstance(hf, str) and re.fullmatch(r"[\w.-]+/[\w.-]+", hf):
86 86 ids["hf_repo"] = hf
87 87 ref = next((e for e in facts.entities if e.entity_type == "model" and e.identifiers.get("openrouter") == base_id), None)
88 88 if ref is None:
89 − ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=[a for a in {item.get("name"), base_id, base_slug} if a and a != name])
89 + ident = model_identity(base_id) # untrusted: aliases only (OpenRouter slugs ≠ vendor API ids)
90 + aliases = [a for a in dict.fromkeys([item.get("name"), base_id, base_slug, *(ident.aliases if ident else [])]) if a and a != name]
91 + ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=aliases, family=family_ref(name, org), identity_confidence="medium")
90 92 if org:
91 93 facts.relate(org, "develops", ref)
92 94 facts.claim(ref, "openrouter_id", base_id)
@@ -96,7 +98,7 @@ class OpenRouterConnector(BaseConnector):
96 98 self._claims(facts, ref, item)
97 99 elif not variant:
98 100 self._claims(facts, ref, item)
99 − price = self._price(facts, ref, item, provider_key, aggregator, full_id, variant)
101 + price = self._price(facts, ref, item, provider_key, vendor, aggregator, full_id, variant)
100 102 if price is None and not variant:
101 103 # still note the listing so `available_through` exists even without a usable price
102 104 facts.claim(ref, "openrouter_listed", True)
@@ -108,12 +110,12 @@ class OpenRouterConnector(BaseConnector):
108 110 facts.claim(ref, "context_length", _int(item.get("context_length")), unit="tokens")
109 111 facts.claim(ref, "max_output_tokens", _int(top.get("max_completion_tokens")), unit="tokens")
110 112 raw_in = [m for m in arch.get("input_modalities") or [] if isinstance(m, str)]
111 − mi = [m for m in raw_in if m != "file"]
112 − mo = [m for m in arch.get("output_modalities") or [] if isinstance(m, str) and m != "file"]
113 + raw_out = [m for m in arch.get("output_modalities") or [] if isinstance(m, str)]
114 + mi, mo = normalize_modalities(raw_in), normalize_modalities(raw_out) # "file" → document (ontology)
113 115 if "file" in raw_in:
114 116 facts.claim(ref, "file_input", True)
115 − facts.claim(ref, "modalities_input", sorted(set(mi)))
116 − facts.claim(ref, "modalities_output", sorted(set(mo)))
117 + facts.claim(ref, "modalities_input", mi)
118 + facts.claim(ref, "modalities_output", mo)
117 119 facts.claim(ref, "modalities", sorted(set(mi) | set(mo)))
118 120 tok = arch.get("tokenizer")
119 121 facts.claim(ref, "tokenizer", tok if isinstance(tok, str) and tok.lower() not in ("router", "other") else None)
@@ -130,14 +132,12 @@ class OpenRouterConnector(BaseConnector):
130 132 facts.claim(ref, "vision", True)
131 133 created = item.get("created")
132 134 if isinstance(created, (int, float)) and created > 1_000_000_000:
133 − listed = datetime.fromtimestamp(created, tz=UTC).date().isoformat()
134 − facts.claim(ref, "openrouter_listed_at", listed)
135 − if "hf_repo" not in ref.identifiers: # hub-hosted weights carry their own creation date (same tier) — API-only models don't
136 − facts.claim(ref, "release_date", listed)
135 + # the catalogue's `created` is when OpenRouter listed the model — a listing date, never the model's release date
136 + facts.claim(ref, "openrouter_listed_at", datetime.fromtimestamp(created, tz=UTC).date().isoformat())
137 137 facts.claim(ref, "knowledge_cutoff", _month(item.get("knowledge_cutoff")))
138 138 facts.claim(ref, "openrouter_expiration_date", item.get("expiration_date") if isinstance(item.get("expiration_date"), str) else None)
139 139
140 − def _price(self, facts: Facts, ref: EntityRef, item: dict[str, Any], provider_key: str | None, aggregator: EntityRef, full_id: str,
140 + def _price(self, facts: Facts, ref: EntityRef, item: dict[str, Any], provider_key: str | None, vendor: str, aggregator: EntityRef, full_id: str,
141 141 variant: str | None) -> Any:
142 142 pricing = item.get("pricing") or {}
143 143 prompt, completion = _per_mtok(pricing.get("prompt")), _per_mtok(pricing.get("completion"))
@@ -145,11 +145,10 @@ class OpenRouterConnector(BaseConnector):
145 145 return None
146 146 if (prompt is not None and prompt < 0) or (completion is not None and completion < 0):
147 147 return None
148 − provider = provider_ref(provider_key) if provider_key else aggregator
149 − if provider is not aggregator:
150 − facts.entities.append(provider)
151 148 top = item.get("top_provider") or {}
152 − features: dict[str, Any] = {"via": "openrouter"}
149 + features: dict[str, Any] = {"via": "openrouter", "upstream_vendor": vendor}
150 + if provider_key:
151 + features["upstream_provider"] = provider_key # registry provider key of the routed lab — the row still belongs to OpenRouter
153 152 if variant:
154 153 features["variant"] = variant
155 154 for key in ("web_search", "internal_reasoning", "audio", "audio_output", "image_output", "input_cache_write_1h"):
@@ -160,29 +159,29 @@ class OpenRouterConnector(BaseConnector):
160 159 features["tiered_pricing"] = pricing["overrides"]
161 160 per_image = _float(pricing.get("image"))
162 161 per_request = _float(pricing.get("request"))
163 − return facts.price(model=ref, provider=provider, provider_model_id=full_id, input_per_mtok=prompt, output_per_mtok=completion,
162 + return facts.price(model=ref, provider=aggregator, provider_model_id=full_id, input_per_mtok=prompt, output_per_mtok=completion,
164 163 cached_input_per_mtok=_per_mtok(pricing.get("input_cache_read")), cache_write_per_mtok=_per_mtok(pricing.get("input_cache_write")),
165 164 per_image=per_image if per_image else None, per_request=per_request if per_request else None,
166 165 context_length=_int(top.get("context_length") or item.get("context_length")), max_output_tokens=_int(top.get("max_completion_tokens")),
167 166 features=features, source_url=f"https://openrouter.ai/{full_id}", meta={"is_moderated": top.get("is_moderated")})
168 167
169 − def _vendor(self, facts: Facts, vendor: str, name: str) -> tuple[EntityRef | None, str | None]:
168 + def _vendor(self, facts: Facts, vendor: str, name: str, base_slug: str) -> tuple[EntityRef | None, str | None]:
170 169 provider_key = provider_by_openrouter(vendor) or VENDOR_PROVIDER.get(vendor)
171 170 if provider_key and provider_key not in providers():
172 171 provider_key = None
173 − org_key = VENDOR_ORG.get(vendor)
172 + org_key = org_key_for_vendor(vendor)
174 173 if not org_key and provider_key:
175 174 org_key = providers()[provider_key].get("organization")
176 175 if not org_key:
177 − known = org_by_hf(vendor)
178 − org_key = known["key"] if known else (vendor if vendor in organizations() else None)
179 − if org_key and org_key in organizations():
180 − org = org_ref(org_key)
181 − else:
176 + ident = model_identity(base_slug)
177 + org_key = ident.org_key if ident else None
178 + org = org_ref_in(facts, org_key)
179 + if org is None:
182 180 display = name.split(":", 1)[0].strip() if ":" in name else vendor
183 − org = EntityRef(entity_type="company", name=display or vendor, identifiers={"openrouter_vendor": vendor}, aliases=[vendor])
184 − if org not in facts.entities:
185 − facts.entities.append(org)
181 + org = next((e for e in facts.entities if e.identifiers.get("openrouter_vendor") == vendor), None)
182 + if org is None:
183 + org = EntityRef(entity_type="company", name=display or vendor, identifiers={"openrouter_vendor": vendor}, aliases=[vendor], identity_confidence="medium")
184 + facts.entities.append(org)
186 185 return org, provider_key
187 186
188 187
modified src/aiatlas/connectors/providers/provider_pricing.py +27 −16
@@ -16,7 +16,8 @@ from __future__ import annotations
16 16 import re
17 17 from typing import Any
18 18
19 −from aiatlas.registry import org_by_hf, org_ref, organizations, provider_ref
19 +from aiatlas.connectors._identity import family_ref, model_identity, org_ref_in
20 +from aiatlas.registry import org_by_hf, organizations, provider_ref
20 21 from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext
21 22 from aiatlas.sdk.extract.numbers import parse_context_length
22 23 from aiatlas.sdk.facts import EntityRef, Facts, Target
@@ -32,28 +33,33 @@ def _money(s: str) -> float | None:
32 33
33 34
34 35 def _org_for(facts: Facts, *candidates: str | None) -> EntityRef | None:
35 − """Registry organization from a vendor slug / display name (hf_org, key, name or alias match)."""
36 + """Registry organization from a vendor slug / display word: exact hf_org, key, name or alias match only — never a guess."""
36 37 for c in candidates:
37 38 if not c:
38 39 continue
39 40 low = c.strip().lower()
40 41 known = org_by_hf(low)
41 42 if known:
42 − ref = org_ref(known["key"])
43 − facts.entities.append(ref)
44 − return ref
43 + return org_ref_in(facts, known["key"])
45 44 for key, o in organizations().items():
46 45 names = {key, o["name"].lower(), *(a.lower() for a in o.get("aliases", []))}
47 46 if low in names:
48 − ref = org_ref("meta-ai" if key == "meta" and "meta-ai" in organizations() else key) # models come from the lab, not the holding
49 − facts.entities.append(ref)
50 − return ref
47 + return org_ref_in(facts, "meta-ai" if key == "meta" and "meta-ai" in organizations() else key) # models come from the lab, not the holding
51 48 return None
52 49
53 50
51 +def _org_for_id(facts: Facts, model_id: str | None, *fallback_words: str | None) -> EntityRef | None:
52 + """Developer organisation of a provider's model id (`openai/gpt-oss-120b`, `fireworks/kimi-k3`, `minimax-m3`): first-party prefix or
53 + family word through the shared identity helper, then an exact registry match on the display words."""
54 + ident = model_identity(model_id) if model_id else None
55 + if ident and ident.org_key:
56 + return org_ref_in(facts, ident.org_key)
57 + return _org_for(facts, *fallback_words)
58 +
59 +
54 60 class _PricingBase(BaseConnector):
55 − version = "1"
56 − parser_version = "1"
61 + version = "2"
62 + parser_version = "2"
57 63 interval_seconds = 6 * 3600
58 64 min_interval_seconds = 3 * 3600
59 65 max_interval_seconds = 3 * 86400
@@ -63,7 +69,11 @@ class _PricingBase(BaseConnector):
63 69 provider_key = ""
64 70
65 71 def _model(self, facts: Facts, name: str, *, ids: dict[str, str], org: EntityRef | None, aliases: list[str]) -> EntityRef:
66 − ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=[a for a in dict.fromkeys(aliases) if a and a != name])
72 + for e in facts.entities:
73 + if e.entity_type == "model" and ids and any(e.identifiers.get(k) == v for k, v in ids.items()):
74 + return e
75 + ref = facts.entity("model", name, identifiers=ids, organization=org, aliases=[a for a in dict.fromkeys(aliases) if a and a != name],
76 + family=family_ref(name, org), identity_confidence="medium")
67 77 if org:
68 78 facts.relate(org, "develops", ref)
69 79 return ref
@@ -121,7 +131,7 @@ class GroqPricingConnector(_PricingBase):
121 131 return
122 132 img = first.css_first("img[alt]")
123 133 vendor = model_id.split("/")[0] if "/" in model_id else None
124 − org = _org_for(facts, vendor, img.attributes.get("alt") if img else None)
134 + org = _org_for_id(facts, model_id, vendor, img.attributes.get("alt") if img else None)
125 135 ref = self._model(facts, name, ids={"groq_model_id": model_id}, org=org, aliases=[model_id, model_id.split("/")[-1]])
126 136 facts.claim(ref, "groq_model_id", model_id)
127 137 texts = [c.text(separator=" ", strip=True) for c in cells]
@@ -151,7 +161,7 @@ class GroqPricingConnector(_PricingBase):
151 161 facts.claim(ref, "groq_pricing_note", texts[2][:120])
152 162 facts.relate(ref, "available_through", provider, attributes={"provider_model_id": model_id, **features})
153 163 if section == "preview":
154 − facts.claim(ref, "groq_status", "preview")
164 + facts.claim(ref, "groq_status", "preview") # Groq's own availability tier; the model's `status` belongs to its lab
155 165
156 166
157 167 # ================================================================================================ Together AI
@@ -207,7 +217,7 @@ class TogetherPricingConnector(_PricingBase):
207 217 if e["input"] is None and e["output"] is None:
208 218 continue
209 219 ids = {"together_ai_model_slug": e["slug"]} if e["slug"] else {}
210 − org = _org_for(facts, e["name"].split(" ")[0])
220 + org = _org_for_id(facts, e["slug"], e["name"].split(" ")[0])
211 221 ref = self._model(facts, e["name"], ids=ids, org=org, aliases=[e["slug"] or ""])
212 222 facts.price(model=ref, provider=provider, provider_model_id=e["slug"], input_per_mtok=e["input"], output_per_mtok=e["output"],
213 223 cached_input_per_mtok=e["cached"], batch_input_per_mtok=e["batch_in"], batch_output_per_mtok=e["batch_out"],
@@ -265,8 +275,9 @@ class FireworksPricingConnector(_PricingBase):
265 275 variant = label[len(base_name):].strip().lower().replace(" ", "-") if label.startswith(base_name) and label != base_name else None
266 276 ref = refs.get(base_key)
267 277 if ref is None:
268 − org = _org_for(facts, base_name.split(" ")[0])
269 − ids = {"fireworks-ai_model_id": slug} if slug else {}
278 + org = _org_for_id(facts, slug, base_name.split(" ")[0])
279 + # canonical scheme `fireworks_model_id`; the historical hyphenated scheme is kept as a second identifier so existing rows keep resolving
280 + ids = {"fireworks_model_id": slug, "fireworks-ai_model_id": slug} if slug else {}
270 281 ref = refs[base_key] = self._model(facts, base_name, ids=ids, org=org, aliases=[slug or "", slug.split("/")[-1] if slug else ""])
271 282 features: dict[str, Any] = {"serving_path": variant or "standard"}
272 283 if i_pri is not None and len(row) > i_pri:
modified src/aiatlas/connectors/research/arxiv.py +28 −8
@@ -6,8 +6,9 @@
6 6 on 2026-09-11 `https://export.arxiv.org/robots.txt` is `User-agent: * / Disallow: /`, so the SDK's robots policy blocks the Atom API
7 7 (3 `blocked_source` review items per run). The RSS feeds are not disallowed and already exceed `expected_min_records`.
8 8
9 −Paper entities are keyed by the version-less arXiv id (`{"arxiv": "2509.01234"}`). Only the first three authors become `researcher`
10 −entities (name only) with `authored` relations. PDFs are not fetched (`needs_llm=False`); a later job builds paper passports.
9 +Paper entities are keyed by the version-less arXiv id (`{"arxiv": "2509.01234"}`). Authors are a claim on the paper; a `researcher`
10 +entity is created only when the feed carries an identifier (ORCID) — never from a bare name. PDFs are not fetched (`needs_llm=False`);
11 +a later job builds paper passports.
11 12 """
12 13 from __future__ import annotations
13 14
@@ -36,8 +37,8 @@ class ArxivConnector(BaseConnector):
36 37 label = "arXiv — recent cs.LG / cs.CL / cs.AI / cs.CV papers (Atom API + RSS)"
37 38 description = "Atom listing of the newest submissions in the core ML categories and the daily category RSS feeds."
38 39 source_key = "arxiv.org"
39 − version = "1"
40 − parser_version = "1"
40 + version = "2"
41 + parser_version = "2"
41 42 interval_seconds = 6 * 3600
42 43 min_interval_seconds = 2 * 3600
43 44 max_interval_seconds = 86400
@@ -109,14 +110,33 @@ class ArxivConnector(BaseConnector):
109 110 facts.claim(ref, "journal_ref", clean_text(jref)[:500] if isinstance(jref, str) and jref.strip() else None)
110 111 if announce:
111 112 facts.claim(ref, "arxiv_announce_type", announce)
112 − for name in authors[:3]:
113 − person = EntityRef(entity_type="researcher", name=name[:200])
114 − facts.entities.append(person)
115 − facts.relate(person, "authored", ref)
113 + # researchers only with an identifier: arXiv feeds carry no ORCID / author id, so authors stay a claim on the paper (homonyms
114 + # would merge and spelling variants split name-only entities). An ORCID in the feed (rare `arxiv:author` extension) creates one.
115 + for author in (e.get("authors") or [])[:20]:
116 + orcid = _orcid(author)
117 + if orcid:
118 + person = EntityRef(entity_type="researcher", name=(author.get("name") or orcid)[:200], identifiers={"orcid": orcid}, identity_confidence="high")
119 + facts.entities.append(person)
120 + facts.relate(person, "authored", ref)
116 121 if doi and isinstance(doi, str) and doi.strip():
117 122 ref.identifiers["doi"] = doi.strip()
118 123
119 124
125 +ORCID = re.compile(r"(\d{4}-\d{4}-\d{4}-\d{3}[\dX])")
126 +
127 +
128 +def _orcid(author: Any) -> str | None:
129 + if not isinstance(author, dict):
130 + return None
131 + for key in ("orcid", "arxiv_orcid", "uri", "href", "id"):
132 + v = author.get(key)
133 + if isinstance(v, str) and "orcid.org" in v.lower() or (isinstance(v, str) and key == "orcid"):
134 + m = ORCID.search(v)
135 + if m:
136 + return m.group(1)
137 + return None
138 +
139 +
120 140 def _arxiv_id(s: str) -> str | None:
121 141 m = ARXIV_ID.search(s)
122 142 if m:
modified src/aiatlas/connectors/research/openreview.py +12 −5
@@ -30,8 +30,8 @@ class OpenReviewConnector(BaseConnector):
30 30 label = "OpenReview — accepted/submitted papers of recent ML conferences"
31 31 description = "Public notes of the configured venue ids (API v2 JSON). Disabled by default: the endpoint currently requires a browser challenge."
32 32 source_key = "openreview.net"
33 − version = "1"
34 − parser_version = "1"
33 + version = "2"
34 + parser_version = "2"
35 35 interval_seconds = 86400
36 36 min_interval_seconds = 12 * 3600
37 37 max_interval_seconds = 7 * 86400
@@ -91,10 +91,17 @@ class OpenReviewConnector(BaseConnector):
91 91 facts.claim(ref, "tldr", tldr if isinstance(tldr, str) else None)
92 92 primary = _val(content.get("primary_area"))
93 93 facts.claim(ref, "primary_area", primary if isinstance(primary, str) else None)
94 − for name in authors[:3]:
95 − person = EntityRef(entity_type="researcher", name=name[:200])
94 + # researchers only with an identifier: OpenReview profile ids (`~Ada_Placeholder1`) — name-only authors stay a claim on the paper
95 + author_ids = _val(content.get("authorids")) or []
96 + author_ids = [a for a in author_ids if isinstance(a, str)] if isinstance(author_ids, list) else []
97 + for i, pid in enumerate(author_ids[:20]):
98 + if not pid.startswith("~"):
99 + continue
100 + name = authors[i] if i < len(authors) else pid.strip("~").replace("_", " ").rstrip("0123456789")
101 + person = EntityRef(entity_type="researcher", name=name[:200], identifiers={"openreview_profile": pid}, aliases=[pid], identity_confidence="high")
96 102 facts.entities.append(person)
97 − facts.relate(person, "authored", ref)
103 + facts.claim(person, "openreview_profile_url", f"https://openreview.net/profile?id={pid}")
104 + facts.relate(person, "authored", ref, attributes={"position": i + 1})
98 105
99 106
100 107 def _val(field: Any) -> Any:
modified src/aiatlas/registry/seed.py +47 −6
@@ -10,6 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncConnection
10 10 from aiatlas.connectors import registry as connector_registry
11 11 from aiatlas.db import execute, fetch_one, jsonb
12 12 from aiatlas.ids import new_id
13 +from aiatlas.ontology.benchmarks import metric_bounds, normalize_metric
14 +from aiatlas.ontology.taxonomy import ORG_TYPE_DEFAULT_KIND, normalize_org_kind
13 15 from aiatlas.registry import load, org_ref, organizations, provider_ref
14 16 from aiatlas.sdk.facts import EntityRef, Facts
15 17 from aiatlas.sdk.writer import FactWriter
@@ -94,7 +96,9 @@ async def _seed_organizations(conn: AsyncConnection, source_id: str) -> int:
94 96 facts.claim(ref, "hf_org", o["hf_org"], source_url=f"https://huggingface.co/{o['hf_org']}")
95 97 if o.get("github_org"):
96 98 facts.claim(ref, "github_org", o["github_org"], source_url=f"https://github.com/{o['github_org']}")
97 − facts.claim(ref, "org_kind", o.get("type", "company"), source_url=o.get("source_url"))
99 + kind = normalize_org_kind(o.get("kind")) or ORG_TYPE_DEFAULT_KIND.get(o.get("type", "company"))
100 + if kind:
101 + facts.claim(ref, "org_kind", kind, source_url=o.get("source_url"))
98 102 if o.get("parent") and o["parent"] in organizations():
99 103 facts.relate(org_ref(o["parent"]), "owns", ref, source_url=o.get("source_url"))
100 104 w = _writer(conn, source_id, o.get("source_url"))
@@ -119,16 +123,53 @@ async def _seed_providers(conn: AsyncConnection, source_id: str) -> int:
119 123 return n
120 124
121 125
126 +def benchmark_entity_ref(b: dict[str, Any]) -> EntityRef:
127 + return EntityRef(entity_type="benchmark", name=b["name"], identifiers={"registry_benchmark": b["key"]}, aliases=list(b.get("aliases", [])), slug_hint=b["key"])
128 +
129 +
130 +def family_heads(entries: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
131 + """family key → representative entry (`family_head: true`, else the entry whose key equals the family key)."""
132 + heads: dict[str, dict[str, Any]] = {}
133 + for b in entries:
134 + fam = b.get("family")
135 + if fam and b.get("family_head"):
136 + heads[fam] = b
137 + for b in entries:
138 + fam = b.get("family")
139 + if fam and fam not in heads and b["key"] == fam:
140 + heads[fam] = b
141 + return heads
142 +
143 +
122 144 async def _seed_benchmarks(conn: AsyncConnection, source_id: str) -> int:
123 145 n = 0
124 − for b in load("benchmarks"):
146 + entries = load("benchmarks")
147 + heads = family_heads(entries)
148 + for b in entries:
125 149 facts = Facts()
126 − ref = EntityRef(entity_type="benchmark", name=b["name"], identifiers={"registry_benchmark": b["key"]}, aliases=list(b.get("aliases", [])), slug_hint=b["key"])
150 + ref = benchmark_entity_ref(b)
127 151 facts.entities.append(ref)
128 − for prop in ("category", "task", "metric", "unit", "creator", "website", "paper", "known_limitations", "methodology"):
152 + src = b.get("source_url")
153 + for prop in ("category", "task", "unit", "creator", "website", "paper", "known_limitations", "methodology", "family", "variant", "version", "harness",
154 + "comparability_note", "metric_label"):
129 155 if b.get(prop) not in (None, ""):
130 − facts.claim(ref, prop, b[prop], source_url=b.get("source_url"))
131 − await _writer(conn, source_id, b.get("source_url")).write(facts)
156 + facts.claim(ref, prop, b[prop], source_url=src)
157 + metric = normalize_metric(b.get("metric")) or b.get("metric")
158 + if metric:
159 + facts.claim(ref, "metric", metric, source_url=src)
160 + if b.get("metric") and b["metric"] != metric:
161 + facts.claim(ref, "metric_raw", b["metric"], source_url=src)
162 + lo, hi = metric_bounds(metric, b.get("unit"))
163 + for prop, fallback in (("metric_min", lo), ("metric_max", hi)):
164 + value = b.get(prop, fallback)
165 + if value is not None:
166 + facts.claim(ref, prop, value, source_url=src)
167 + facts.claim(ref, "higher_is_better", bool(b.get("higher_is_better", True)), source_url=src)
168 + facts.claim(ref, "family_head", bool(b.get("family_head", False)), source_url=src)
169 + head = heads.get(b.get("family") or "")
170 + if head and head["key"] != b["key"]:
171 + facts.relate(ref, "variant_of", benchmark_entity_ref(head), attributes={"family": b["family"], "variant": b.get("variant")}, source_url=src)
172 + await _writer(conn, source_id, src).write(facts)
132 173 n += 1
133 174 return n
134 175
modified tests/test_arxiv.py +3 −2
@@ -26,8 +26,9 @@ async def test_atom(connector):
26 26 assert c["primary_category"] == "cs.CV" and c["published_at"] == "2026-09-10T17:59:55+00:00"
27 27 assert c["pdf_url"] == "https://arxiv.org/pdf/2609.11929" and c["authors"][0] == "Haiwen Diao" and c["abstract"].startswith("We launch SenseNova")
28 28 assert c["comment"].startswith("Project page")
29 − authored = [r for r in facts.relations if r.predicate == "authored" and r.object is papers[0]]
30 − assert len(authored) == 3 and all(r.subject.entity_type == "researcher" for r in authored)
29 + # no name-only researchers any more: the Atom feed carries no author identifier (ORCID), so authors stay a claim on the paper
30 + assert not any(e.entity_type == "researcher" for e in facts.entities) and not any(r.predicate == "authored" for r in facts.relations)
31 + assert len(c["authors"]) >= 3
31 32 assert not any(t.needs_llm for t in facts.targets) and facts.targets == []
32 33
33 34
modified tests/test_cohere.py +7 −2
@@ -16,7 +16,9 @@ async def test_models(connector):
16 16 assert len(models) >= 40 and {"command-a-plus-05-2026", "embed-v4.0", "rerank-v4.0-pro", "parse-v5.0", "cohere-transcribe-03-2026", "tiny-aya-global"} <= models
17 17 a_plus = claims_of(facts, "command-a-plus-05-2026")
18 18 assert a_plus["context_length"] == 128_000 and a_plus["max_output_tokens"] == 64_000 and a_plus["status"] == "active"
19 − assert a_plus["modalities"] == ["text", "image"] and a_plus["vision"] is True and a_plus["family"] == "Command" and a_plus["is_moe"] is True
19 + assert a_plus["modalities"] == ["image", "text"] and a_plus["vision"] is True and a_plus["family"] == "Command" and a_plus["is_moe"] is True # canonical, sorted
20 + a_plus_ref = next(e for e in facts.entities if e.name == "command-a-plus-05-2026")
21 + assert a_plus_ref.family is not None and a_plus_ref.family.entity_type == "model_family" and a_plus_ref.family.name == "Command"
20 22 assert a_plus["foundry_model_id"] == "coherelabs-command-a-plus-05-2026-w4a4"
21 23 r = claims_of(facts, "command-r-03-2024")
22 24 assert r["status"] == "deprecated" and r["deprecation_date"] == "2025-09-15"
@@ -25,12 +27,15 @@ async def test_models(connector):
25 27 north = claims_of(facts, "north-small-translate-1-0")
26 28 assert north["parameter_count"] == 218_000_000_000 and north["active_parameter_count"] == 25_000_000_000 and north["is_moe"] is True
27 29 assert claims_of(facts, "rerank-v3.5")["bedrock_model_id"] == "cohere.rerank-v3-5:0"
28 − assert claims_of(facts, "cohere-transcribe-03-2026")["openness"] == "open-source"
30 + transcribe = claims_of(facts, "cohere-transcribe-03-2026")
31 + assert transcribe["openness"] == "open-weights" and transcribe["openness_raw"] == "open-source" and transcribe["weights_available"] is True # prose "open source" → canonical category
29 32 assert claims_of(facts, "command-a-reasoning-08-2025")["reasoning"] is True
30 33 # alias rows fold into the model they point at
31 34 plus = next(e for e in facts.entities if e.name == "command-r-plus-04-2024")
32 35 assert "command-r-plus" in plus.aliases and "command-r-plus" not in models
33 36 assert plus.identifiers == {"cohere_model_id": "command-r-plus-04-2024"}
37 + plus_claims = claims_of(facts, "command-r-plus-04-2024")
38 + assert plus_claims["api_aliases"] == ["command-r-plus"] and plus_claims["api_alias"] == "command-r-plus" # always a list + first for compat
34 39 assert sum(r.predicate == "available_through" for r in facts.relations) >= 30
35 40
36 41
modified tests/test_github_pypi.py +11 −2
@@ -1,7 +1,8 @@
1 1 import pytest
2 2
3 −from aiatlas.connectors.code.github import GitHubConnector, repo_entries
3 +from aiatlas.connectors.code.github import GitHubConnector, entity_type_for, repo_entries
4 4 from aiatlas.connectors.code.pypi import PyPIConnector, _license, _norm
5 +from aiatlas.ontology.taxonomy import FRAMEWORK_KINDS
5 6 from aiatlas.registry import organizations
6 7 from aiatlas.sdk.facts import Target
7 8 from tests.conftest import claims_of, extract_from_fixture, fixture_path
@@ -10,8 +11,15 @@ from tests.conftest import claims_of, extract_from_fixture, fixture_path
10 11 def test_repositories_registry():
11 12 entries = repo_entries()
12 13 assert len(entries) >= 70 and len({e["key"] for e in entries}) == len(entries)
13 − assert all(e["kind"] in {"framework", "runtime", "library", "agent", "tool", "model"} and e["repo"].count("/") == 1 for e in entries)
14 + assert all((e["kind"] == "model" or e["kind"] in FRAMEWORK_KINDS) and e["repo"].count("/") == 1 for e in entries) # canonical kinds
14 15 assert all(e["organization"] in organizations() for e in entries if e.get("organization"))
16 + agents = [e for e in entries if e["kind"] == "agent"]
17 + assert {e["key"] for e in agents} == {"claude-code", "codex", "gemini-cli", "openhands", "aider", "browser-use"}
18 + assert all(e.get("agent_kind") in ("coding", "browser", "research") for e in agents)
19 + assert entity_type_for("agent") == "agent" and entity_type_for("application") == "tool" and entity_type_for("mcp-server") == "tool"
20 + assert entity_type_for("model") == "repository" and entity_type_for("inference-engine") == "framework" and entity_type_for("library") == "framework"
21 + tools = [e["key"] for e in entries if entity_type_for(e["kind"]) == "tool"]
22 + assert {"comfyui", "lms", "text-generation-webui", "stable-diffusion-webui", "mcp-servers"} <= set(tools)
15 23
16 24
17 25 @pytest.fixture
@@ -32,6 +40,7 @@ async def test_github_repo_page(gh):
32 40 assert vllm.identifiers == {"github_repo": "vllm-project/vllm", "pypi": "vllm"} and vllm.organization.name == "vLLM project"
33 41 c = claims_of(facts, "vllm")
34 42 assert c["metric.stars"] == 91512 and c["metric.forks"] == 22070 and c["license"] == "Apache-2.0" and c["metric.releases"] == 105
43 + assert c["kind"] == "inference-engine" and "kind_raw" not in c and "license_raw" not in c # already canonical in the seed / SPDX
35 44 assert c["description"].startswith("A high-throughput") and c["homepage"] == "https://vllm.ai" and "llm-serving" in c["topics"]
36 45 assert c["created_at"] == "2023-02-09T11:23:20+00:00" and "language" not in c # language bar is client-rendered
37 46
modified tests/test_google.py +10 −6
@@ -17,17 +17,20 @@ async def test_models_overview(connector):
17 17 assert len(models) >= 40
18 18 assert {"Gemini 3.8 Flash", "Gemini 2.5 Pro", "Nano Banana 2", "Veo 3.1", "Gemini Embedding 2"} <= models
19 19 flash = claims_of(facts, "Gemini 3.8 Flash")
20 − assert flash["api_model_id"] == "gemini-3.8-flash" and flash["status"] == "active" and flash["family"] == "Gemini"
20 + assert flash["api_model_id"] == "gemini-3.8-flash" and flash["status"] == "active" and flash["family"] == "Gemini 3.8" # versioned family (ontology)
21 + flash_ref = next(e for e in facts.entities if e.name == "Gemini 3.8 Flash")
22 + assert flash_ref.family.entity_type == "model_family" and flash_ref.family.name == "Gemini 3.8" and flash_ref.family.organization.name == "Google"
21 23 assert claims_of(facts, "Gemini 3.1 Pro")["status"] == "preview"
22 24 assert claims_of(facts, "Gemini 2.0 Flash")["status"] == "retired"
23 25 ref = next(e for e in facts.entities if e.name == "Nano Banana 2")
24 − assert ref.identifiers == {"gemini_model_id": "gemini-3.1-flash-image"}
26 + assert ref.identifiers == {"gemini_model_id": "gemini-3.1-flash-image", "google_model_id": "gemini-3.1-flash-image"} # both schemes
25 27 pages = [t for t in facts.targets if t.doc_type == "model_page"]
26 28 assert len(pages) >= 40 and pages[0].url == f"{DOCS}/models/gemini-3.8-flash"
27 29 # several endpoints under one display name: identity = first endpoint, the others are a claim (never aliases)
28 30 transcribe = next(e for e in facts.entities if e.name == "Gemini 3.5 Transcribe")
29 − assert transcribe.identifiers == {"gemini_model_id": "gemini-3.5-transcribe"} and "gemini-3.5-transcribe-live" not in transcribe.aliases
31 + assert transcribe.identifiers["gemini_model_id"] == "gemini-3.5-transcribe" and "gemini-3.5-transcribe-live" not in transcribe.aliases
30 32 assert claims_of(facts, "Gemini 3.5 Transcribe")["api_aliases"] == ["gemini-3.5-transcribe-live"]
33 + assert claims_of(facts, "Gemini 3.5 Transcribe")["api_alias"] == "gemini-3.5-transcribe-live"
31 34 assert len([e for e in facts.entities if e.name == "Gemini Embedding 2"]) == 1
32 35 omni = {e.name: e.identifiers["gemini_model_id"] for e in facts.entities if e.entity_type == "model" and e.name.startswith("Gemini Omni Flash")}
33 36 assert omni == {"Gemini Omni Flash": "gemini-omni-flash"} # the GA endpoint row folds into the card's api_aliases
@@ -44,9 +47,10 @@ async def test_model_pages(connector):
44 47 facts = await extract_from_fixture(connector, target, fixture_path("google", "model-gemini-3.8-flash.html"))
45 48 m = claims_of(facts, "Gemini 3.8 Flash")
46 49 assert m["context_length"] == 1_048_576 and m["max_output_tokens"] == 65_536
47 − assert m["modalities_input"] == ["text", "image", "video", "audio", "pdf"] and m["modalities_output"] == ["text"]
48 − assert m["tool_calling"] is True and m["structured_output"] is True and m["reasoning"] is True and m["vision"] is True
49 − assert "Function calling" in m["capabilities"] and "Image generation" not in m["capabilities"]
50 + assert m["modalities_input"] == ["audio", "document", "image", "text", "video"] and m["modalities_output"] == ["text"] # pdf → document (ontology)
51 + assert m["tool_calling"] is True and m["structured_output"] is True and m["reasoning"] is True and m["vision"] is True and m["audio"] is True
52 + assert {"function_calling", "structured_output", "reasoning"} <= set(m["capabilities"]) and "image_generation" not in m["capabilities"]
53 + assert "Function calling" in m["capabilities_raw"] and "Image generation" not in m["capabilities_raw"] # Google's labels kept as raw
50 54 assert m["latest_update"] == "2026-09" and m["versions"] == {"stable": ["gemini-3.8-flash"]}
51 55 target = Target(url=f"{DOCS}/models/gemini-2.5-pro", doc_type="model_page", key="model:gemini-2.5-pro", meta={"api_id": "gemini-2.5-pro"})
52 56 facts = await extract_from_fixture(connector, target, fixture_path("google", "model-gemini-2.5-pro.html"))
modified tests/test_huggingface.py +42 −10
@@ -1,6 +1,6 @@
1 1 import pytest
2 2
3 −from aiatlas.connectors.hub.huggingface import HuggingFaceConnector
3 +from aiatlas.connectors.hub.huggingface import HuggingFaceConnector, artifact_kind, canonical_name, pipeline_modalities
4 4 from aiatlas.sdk.facts import Target
5 5 from tests.conftest import claims_of, entity_names, extract_from_fixture, fixture_path
6 6
@@ -23,44 +23,65 @@ async def test_listing(connector):
23 23 target = Target(url="https://huggingface.co/models?author=Qwen&sort=downloads&p=0", doc_type="listing", key="listing:Qwen:0", meta={"hf_org": "Qwen", "page": 0})
24 24 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "listing-qwen.html"))
25 25 models = [e for e in facts.entities if e.entity_type == "model"]
26 − assert len(models) == 20 # models_per_org cap
26 + artifacts = [e for e in facts.entities if e.entity_type == "artifact"]
27 + assert len(models) + len(artifacts) == 20 and len(artifacts) == 3 # models_per_org cap; Qwen's own FP8 repos are artifacts
27 28 assert models[0].identifiers == {"hf_repo": "Qwen/Qwen3-0.6B"} and models[0].organization.name == "Qwen"
29 + assert models[0].family is not None and models[0].family.entity_type == "model_family" and models[0].family.name == "Qwen3"
30 + fp8 = next(a for a in artifacts if a.name == "Qwen/Qwen3.8-27B-FP8")
31 + assert fp8.artifact_kind == "quantization" and fp8.canonical is not None and fp8.canonical.name == "Qwen3.8-27B"
32 + assert fp8.canonical.identifiers == {"hf_repo": "Qwen/Qwen3.8-27B"} # the official repo appears later in the same listing → one ref
33 + assert sum(1 for e in models if e.name == "Qwen3.8-27B") == 1
28 34 c = claims_of(facts, "Qwen3-0.6B")
29 − assert c["metric.downloads"] == 20685071 and c["pipeline_tag"] == "text-generation" and c["openness"] == "open-weights"
35 + assert c["metric.downloads"] == 20685071 and c["pipeline_tag"] == "text-generation" and c["access"] == "open" and c["weights_available"] is True
36 + assert c["modalities_input"] == ["text"] and c["modalities_output"] == ["text"] and "openness" not in c # openness needs the licence (model page)
30 37 assert c["parameter_count"] == 751632384
31 38 assert [t.url for t in facts.targets][:2] == ["https://huggingface.co/Qwen/Qwen3-0.6B", "https://huggingface.co/Qwen/Qwen3-VL-8B-Instruct"]
32 − assert all(t.doc_type == "model_page" for t in facts.targets)
39 + assert all(t.doc_type == "model_page" for t in facts.targets) and len(facts.targets) == 20
40 + assert not any(r.predicate == "develops" and r.object.entity_type == "artifact" for r in facts.relations)
33 41
34 42
35 43 async def test_model_page(connector):
36 44 target = Target(url="https://huggingface.co/Qwen/Qwen3-8B", doc_type="model_page", key="model:Qwen/Qwen3-8B", meta={"hf_repo": "Qwen/Qwen3-8B"})
37 45 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "model-qwen3-8b.html"))
38 46 c = claims_of(facts, "Qwen3-8B")
39 − assert c["license"] == "apache-2.0" and c["library_name"] == "transformers" and c["architecture"] == "Qwen3ForCausalLM"
47 + assert c["license"] == "Apache-2.0" and c["license_raw"] == "apache-2.0" and c["library_name"] == "transformers" and c["architecture"] == "Qwen3ForCausalLM"
40 48 assert c["parameter_count"] == 8190735360 and c["release_date"] == "2025-04-27" and c["openness"] == "open-weights"
49 + assert c["access"] == "open" and c["weights_available"] is True and "gated" not in c
41 50 assert c["metric.likes"] == 1366 and c["file_size_gb"] == 16.38 and c["base_model"] == ["Qwen/Qwen3-8B-Base"]
42 51 rels = {(r.subject.name, r.predicate, r.object.name) for r in facts.relations}
43 52 assert ("Qwen3-8B", "fine_tuned_from", "Qwen3-8B-Base") in rels and ("Qwen", "develops", "Qwen3-8B") in rels
44 53 assert ("Qwen3-8B", "described_by", "arXiv:2505.09388") in rels
45 54 assert [t.url for t in facts.targets] == ["https://huggingface.co/Qwen/Qwen3-8B/raw/main/README.md"]
46 55 assert facts.document_entity is not None and facts.document_entity.identifiers == {"hf_repo": "Qwen/Qwen3-8B"}
56 + assert facts.document_entity.entity_type == "model" and facts.document_entity.family.name == "Qwen3" and facts.document_entity.identity_confidence == "high"
47 57
48 58
49 59 async def test_model_page_gguf_and_gated(connector):
50 60 target = Target(url="https://huggingface.co/bartowski/Qwen3.8-27B-GGUF", doc_type="model_page", key="x", meta={"hf_repo": "bartowski/Qwen3.8-27B-GGUF"})
51 61 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "model-bartowski-gguf.html"))
52 − c = claims_of(facts, "bartowski/Qwen3.8-27B-GGUF") # redistributor repos keep the full id as name
62 + art = next(e for e in facts.entities if e.entity_type == "artifact")
63 + assert art.name == "bartowski/Qwen3.8-27B-GGUF" and art.artifact_kind == "quantization" and art.identifiers == {"hf_repo": "bartowski/Qwen3.8-27B-GGUF"}
64 + assert art.canonical is not None and art.canonical.entity_type == "model" and art.canonical.name == "Qwen3.8-27B"
65 + assert art.canonical.organization.name == "Qwen" and art.canonical.identity_confidence == "medium" and art.canonical.family.name == "Qwen3.8"
66 + c = claims_of(facts, "bartowski/Qwen3.8-27B-GGUF") # artifacts keep the full id as name
53 67 assert c["quant_format"] == "gguf" and "Q4_K_M" in c["quantization"] and c["is_quantized"] is True and c["quantized_by"] == "bartowski"
54 − assert c["parameter_count"] == 27_000_000_000 # literally in the repo name
68 + assert c["artifact_kind"] == "quantization" and c["parameter_count"] == 27_000_000_000 # the packaged size stays on the artifact…
69 + assert "parameter_count" not in claims_of(facts, "Qwen3.8-27B") # …never on the canonical model
70 + assert c["modalities_input"] == ["image", "text"] and c["modalities_output"] == ["text"] # image-text-to-text
55 71 assert facts.entities[0].name == "bartowski" and facts.entities[0].identifiers["registry_org"] == "bartowski"
72 + rels = {(r.subject.name, r.predicate, r.object.name) for r in facts.relations}
73 + assert ("bartowski/Qwen3.8-27B-GGUF", "published_by", "bartowski") in rels and not any(p == "develops" for _s, p, _o in rels)
56 74
57 75 target = Target(url="https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct", doc_type="model_page", key="x", meta={"hf_repo": "meta-llama/Llama-3.1-8B-Instruct"})
58 76 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "model-llama-gated.html"))
59 77 c = claims_of(facts, "Llama-3.1-8B-Instruct")
60 − assert c["openness"] == "restricted" and c["gated"] == "manual" and c["license"] == "llama3.1"
78 + assert c["access"] == "gated" and c["gated_mode"] == "manual" and "restricted" not in str(c.get("status"))
79 + assert c["license"] == "Llama-3.1-Community" and c["license_raw"] == "llama3.1" and c["openness"] == "restricted-weights" and c["weights_available"] is True
61 80 assert c["languages"] == ["en", "de", "fr", "it", "pt", "hi", "es", "th"]
62 81 assert facts.targets == [] # gated card is not fetched (401 without auth)
63 82 assert any(r.predicate == "fine_tuned_from" and r.attributes.get("base_model_relation") == "finetune" for r in facts.relations)
83 + llama = next(e for e in facts.entities if e.name == "Llama-3.1-8B-Instruct")
84 + assert llama.entity_type == "model" and llama.family.name == "Llama 3.1" and llama.family.organization.name == "Meta AI"
64 85
65 86
66 87 async def test_model_card(connector):
@@ -68,18 +89,29 @@ async def test_model_card(connector):
68 89 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "readme-bartowski-gguf.md"), content_type="text/markdown")
69 90 assert claims_of(facts, "bartowski/Qwen3.8-27B-GGUF")["base_model"] == ["Qwen/Qwen3.8-27B"]
70 91 assert ("bartowski/Qwen3.8-27B-GGUF", "quantized_from", "Qwen3.8-27B") in {(r.subject.name, r.predicate, r.object.name) for r in facts.relations}
92 + art = next(e for e in facts.entities if e.entity_type == "artifact")
93 + assert art.canonical is not None and art.canonical.identifiers == {"hf_repo": "Qwen/Qwen3.8-27B"} and art.identity_confidence == "high"
71 94 target = Target(url="https://huggingface.co/Qwen/Qwen3-8B/raw/main/README.md", doc_type="model_card", key="x", meta={"hf_repo": "Qwen/Qwen3-8B"})
72 95 facts = await extract_from_fixture(connector, target, fixture_path("huggingface", "readme-qwen3-8b.md"), content_type="text/markdown")
73 − assert claims_of(facts, "Qwen3-8B")["license"] == "apache-2.0"
96 + assert claims_of(facts, "Qwen3-8B")["license"] == "Apache-2.0"
74 97 assert not any(r.predicate in ("derived_from", "quantized_from") for r in facts.relations) # typed relation comes from the page
75 98
76 99
77 −def test_derived_repo_naming():
100 +def test_identity_helpers():
78 101 from aiatlas.connectors.hub.huggingface import _foreign_family
79 102
80 103 assert _foreign_family("NousResearch", "Meta-Llama-3.1-70B-Instruct") is True and _foreign_family("meta-llama", "Llama-3.1-70B-Instruct") is False
81 104 assert _foreign_family("NousResearch", "Hermes-4-70B") is False and _foreign_family("Qwen", "Qwen3-8B") is False
82 105 assert _foreign_family("mlx-community", "Kimi-K2.5") is True
106 + assert canonical_name("Qwen3.8-27B-GGUF") == "Qwen3.8-27B" and canonical_name("Llama-3.1-8B-Instruct-bnb-4bit") == "Llama-3.1-8B-Instruct"
107 + assert canonical_name("DeepSeek-V3-0324-UD-Q4_K_XL") == "DeepSeek-V3-0324" and canonical_name("Kimi-K2.5-bf16") == "Kimi-K2.5"
108 + assert artifact_kind("zai-org/GLM-5-FP8", bases=[], quant_format=None) == "quantization"
109 + assert artifact_kind("mlx-community/Kimi-K2.5", bases=[], quant_format=None) == "packaging"
110 + assert artifact_kind("unsloth/Qwen3-8B-bf16", bases=[], quant_format=None) == "conversion"
111 + assert artifact_kind("Qwen/Qwen3-8B", bases=[("Qwen/Qwen3-8B-Base", "finetune")], quant_format=None) is None
112 + assert artifact_kind("NousResearch/Hermes-4-70B", bases=[("meta-llama/Llama-3.1-70B", "finetune")], quant_format=None) is None # fine-tune = model
113 + assert pipeline_modalities("image-text-to-text") == (["image", "text"], ["text"]) and pipeline_modalities("automatic-speech-recognition") == (["audio"], ["text"])
114 + assert pipeline_modalities("text-to-image") == (["text"], ["image"]) and pipeline_modalities("unknown-tag") == ([], [])
83 115
84 116
85 117 async def test_daily_papers(connector):
modified tests/test_leaderboards.py +94 −12
@@ -1,6 +1,7 @@
1 1 import json
2 2 from collections import Counter
3 3
4 +from aiatlas.connectors._identity import model_identity, split_effort_label, strip_effort
4 5 from aiatlas.connectors.benchmarks.leaderboards import (
5 6 AiderLeaderboardConnector,
6 7 ArtificialAnalysisConnector,
@@ -16,12 +17,30 @@ async def test_aider_polyglot():
16 17 c = AiderLeaderboardConnector()
17 18 target = Target(url=c.URL, doc_type="leaderboard", key="aider")
18 19 facts = await extract_from_fixture(c, target, fixture_path("leaderboards", "aider-polyglot.yml"), content_type="text/plain")
19 − assert facts.entities[0].identifiers == {"registry_benchmark": "aider-polyglot"}
20 + benches = {e.identifiers["registry_benchmark"] for e in facts.entities if e.entity_type == "benchmark"}
21 + assert benches == {"aider-polyglot", "aider-polyglot-well-formed"}
20 22 r = next(r for r in facts.results if r.model.name == "Gemini 2.0 Pro exp-02-05" and r.metric == "pass_rate_2")
21 23 assert r.score == 35.6 and r.unit == "%" and r.config["edit_format"] == "whole" and r.config["date"] == "2025-02-25"
22 − assert r.config["api_model_id"] == "gemini/gemini-2.0-pro-exp-02-05" and r.model.aliases == [] and r.evaluated_at.year == 2025
23 − assert Counter(r.metric for r in facts.results)["percent_cases_well_formed"] == len(facts.results) // 2
24 − assert len(facts.results) >= 100
24 + assert r.config["api_model_id"] == "gemini/gemini-2.0-pro-exp-02-05" and r.evaluated_at.year == 2025
25 + assert r.run_group == "2025-02-25" and r.trust_level == "official-benchmark" and r.variant == "polyglot"
26 + assert r.model.identifiers == {"gemini_model_id": "gemini-2.0-pro-exp-02-05"} and r.model.organization.name == "Google" # pinned, genuine API id
27 + assert "gemini-2.0-pro-exp-02-05" in r.model.aliases and r.model.identity_confidence == "medium"
28 + assert not any(k in r.config for k in ("total_cost_usd", "seconds_per_case", "command", "dirname", "versions", "test_cases")) # bookkeeping dropped
29 + by_bench = Counter(r.benchmark.identifiers["registry_benchmark"] for r in facts.results)
30 + assert by_bench["aider-polyglot-well-formed"] == by_bench["aider-polyglot"] >= 60
31 + wf = next(r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "aider-polyglot-well-formed")
32 + assert wf.metric == "percent_cases_well_formed" and wf.variant == "well-formed"
33 + # effort labels are configurations of one model, never models
34 + gpt5 = [r for r in facts.results if r.model.name == "gpt-5" and r.metric == "pass_rate_2"]
35 + assert sorted(r.config["reasoning_effort"] for r in gpt5) == ["high", "low", "medium"] and len({id(r.model) for r in gpt5}) == 1
36 + assert not any(e.name.endswith(("(high)", "(low)", "(medium)")) for e in facts.entities)
37 + sonnet = [r for r in facts.results if r.model.name == "claude-3-7-sonnet-20250219" and r.metric == "pass_rate_2"]
38 + assert {r.config.get("reasoning") for r in sonnet} == {"on", "off"} and next(r for r in sonnet if r.config.get("reasoning") == "on").config["thinking_budget"] == "32k"
39 + assert sonnet[0].model.identifiers == {"anthropic_model_id": "claude-3-7-sonnet-20250219"} and sonnet[0].model.organization.name == "Anthropic"
40 + rolling = next(r for r in facts.results if r.model.name == "DeepSeek V3 (0324)")
41 + assert rolling.model.identifiers == {} and "deepseek-chat" in rolling.model.aliases and rolling.model.organization.name == "DeepSeek" # rolling id → alias only
42 + assert not any(" + " in e.name for e in facts.entities) # architect two-model systems skipped
43 + assert len(facts.results) >= 120
25 44
26 45
27 46 async def test_swebench():
@@ -30,10 +49,17 @@ async def test_swebench():
30 49 benches = {e.identifiers["registry_benchmark"] for e in facts.entities if e.entity_type == "benchmark"}
31 50 assert benches == {"swe-bench-verified", "swe-bench-lite", "swe-bench-full", "swe-bench-multimodal", "swe-bench-multilingual"}
32 51 verified = [r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "swe-bench-verified"]
33 − assert 35 <= len(verified) <= 40 and not any("&" in r.model.name for r in facts.results) # multi-model systems skipped
52 + assert 25 <= len(verified) <= 40 and not any("&" in r.model.name for r in facts.results) # multi-model systems skipped
53 + assert not any(r.model.name in ("Multiple", "Undisclosed") for r in facts.results) # unnamed models skipped
34 54 top = next(r for r in verified if r.config["system"] == "Sonar Foundation Agent")
35 55 assert top.model.name == "Claude 4.5 Opus" and top.config["model_tag"] == "claude-opus-4-5" and top.model.organization.name == "Anthropic"
56 + assert top.model.identifiers == {} and "claude-opus-4-5" in top.model.aliases # free-text tag → alias, never an identifier
36 57 assert top.score == 79.2 and top.metric == "resolved" and top.config["date"] == "2025-12-05" and top.config["open_source_system"] is False
58 + assert top.trust_level == "community" and top.run_group == "2025-12-05" and top.variant == "Verified"
59 + assert not any(k in top.config for k in ("total_cost_usd", "cost_per_instance_usd"))
60 + assert any(r.trust_level == "official-benchmark" and r.config["checked_by_swebench"] is True for r in facts.results)
61 + gemini = next(r for r in facts.results if r.model.name == "Gemini 3 Flash")
62 + assert gemini.model.organization.name == "Google" and gemini.model.family.name == "Gemini 3" # pattern org = the lab connector's org
37 63
38 64
39 65 async def test_livebench_chain():
@@ -50,10 +76,23 @@ async def test_livebench_chain():
50 76 fixture_path("leaderboards", "livebench-table-2026_06_25.csv"), content_type="text/csv")
51 77 overall = [r for r in facts.results if r.metric == "global_average"]
52 78 assert len(overall) >= 50 and overall[0].score == 72.582 and overall[0].config["release"] == "2026-06-25"
53 − assert any(r.metric == "category:Reasoning" for r in facts.results)
79 + assert all(r.benchmark.identifiers["registry_benchmark"] == "livebench" and r.run_group == "2026-06-25" and r.trust_level == "official-benchmark" for r in overall)
80 + by_bench = Counter(r.benchmark.identifiers["registry_benchmark"] for r in facts.results)
81 + assert by_bench == {k: len(overall) for k in ("livebench", "livebench-reasoning", "livebench-coding", "livebench-agentic-coding", "livebench-mathematics",
82 + "livebench-data-analysis", "livebench-language", "livebench-if")}
83 + reasoning = next(r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "livebench-reasoning")
84 + assert reasoning.metric == "average score" and reasoning.variant == "Reasoning" and reasoning.config["variant"] == "Reasoning"
85 + assert not any(r.metric.startswith("category:") for r in facts.results) # no mixed metrics on one board any more
54 86 glm = next((r for r in overall if r.model.name == "GLM-5.3"), None)
55 87 assert glm is not None and glm.model.identifiers == {} and glm.model.organization.name == "Z.ai (Zhipu AI)"
56 88 assert claims_of(facts, "GLM-5.3")["livebench_hf_link"] == "https://huggingface.co/zai-org/GLM-5.3"
89 + # effort ids fold into the base model with the setting in the configuration
90 + opus = next(r for r in overall if r.config["livebench_model_id"] == "claude-opus-4-6-thinking-auto-high-effort")
91 + assert opus.model.name == "Claude 4.6 Opus" and opus.config["reasoning_effort"] == "high" and opus.config["reasoning"] == "on" and opus.config["thinking_budget"] == "auto"
92 + assert "claude-opus-4-6" in opus.model.aliases and opus.model.organization.name == "Anthropic" and opus.model.identity_confidence == "medium"
93 + kimi = next(r for r in overall if r.config["livebench_model_id"] == "kimi-k2.6-thinking")
94 + assert kimi.model.name == "Kimi K2.6" and kimi.config["reasoning"] == "on"
95 + assert not any(e.name.lower().endswith(("effort", "xhigh", " high")) for e in facts.entities if e.entity_type == "model")
57 96
58 97
59 98 async def test_artificial_analysis():
@@ -63,13 +102,56 @@ async def test_artificial_analysis():
63 102 assert len(models) == 80 and version == "4.3"
64 103 facts = await extract_from_fixture(c, Target(url=c.URL, doc_type="leaderboard", key="aa_models"), fixture_path("leaderboards", "artificial-analysis-models.html"))
65 104 idx = [r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "artificial-analysis-intelligence-index"]
66 − assert len(idx) == 80 and all(r.config["version"] == "4.3" for r in idx) and idx[0].unit == ""
105 + assert len(idx) == 80 and all(r.config["version"] == "4.3" and r.run_group == "4.3" and r.trust_level == "independent-evaluator" for r in idx) and idx[0].unit == ""
106 + entities = [e for e in facts.entities if e.entity_type == "model"]
107 + assert len(entities) < 80 # effort variants folded → fewer models, same results
67 108 mistral = next(r for r in idx if r.model.identifiers == {"artificial_analysis": "mistral-medium"})
68 − assert mistral.score == 5.49 and mistral.model.organization.name == "Mistral AI" and mistral.config["estimated"] is True
109 + assert mistral.score == 5.49 and mistral.model.name == "Mistral Medium" and mistral.model.organization.name == "Mistral AI" and mistral.config["estimated"] is True
110 + assert "reasoning_effort" not in mistral.config # "-medium" is a size tier here, not an effort
111 + opus = next(r for r in idx if r.config["aa_slug"] == "claude-opus-5-xhigh")
112 + assert opus.model.name == "Claude Opus 5" and opus.config["reasoning_effort"] == "xhigh" and opus.model.organization.name == "Anthropic"
113 + assert "artificial_analysis" not in opus.model.identifiers and "claude-opus-5" in opus.model.aliases # AA does not list the base slug → alias only
114 + gpt = next(r for r in idx if r.config["aa_slug"] == "gpt-5-5")
115 + assert gpt.model.name == "GPT-5.5" and gpt.config["reasoning_effort"] == "xhigh" # "(xhigh)" label on a bare slug is still a setting
116 + kimi = [r for r in idx if r.model.name == "Kimi K2.6"]
117 + assert len(kimi) == 2 and len({id(r.model) for r in kimi}) == 1 and {r.config.get("reasoning") for r in kimi} == {"on", "off"}
118 + assert kimi[0].model.identifiers == {"artificial_analysis": "kimi-k2-6"} # base listed by AA → identifier on the base
119 + assert not any(e.name.lower().endswith(("(xhigh)", "(high)", "(medium)", "(low)", "(minimal)", "(non-reasoning)", "(reasoning)")) for e in entities)
120 + assert any(e.name == "Mistral Large 2 (Jul)" for e in entities) # snapshot month labels are not settings
69 121 benches = Counter(r.benchmark.identifiers["registry_benchmark"] for r in facts.results)
70 − assert benches["gpqa"] >= 50 and benches["humanitys-last-exam"] >= 50 and benches["terminal-bench"] >= 50
71 − gpqa = next(r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "gpqa")
72 − assert 0 < gpqa.score <= 100 and gpqa.unit == "%" and gpqa.config["evaluator"] == "Artificial Analysis"
73 − some = next(e for e in facts.entities if e.entity_type == "model" and claims_of(facts, e.name).get("aa_openness"))
122 + assert benches["gpqa-diamond"] >= 50 and benches["humanitys-last-exam"] >= 50 and benches["terminal-bench"] >= 50 and "gpqa" not in benches
123 + gpqa = next(r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "gpqa-diamond")
124 + assert 0 < gpqa.score <= 100 and gpqa.unit == "%" and gpqa.config["evaluator"] == "Artificial Analysis" and gpqa.variant == "Diamond"
125 + tau = next(r for r in facts.results if r.benchmark.identifiers["registry_benchmark"] == "tau2-bench")
126 + assert tau.variant == "Telecom" and tau.config["variant"] == "Telecom"
127 + some = next(e for e in entities if claims_of(facts, e.name).get("aa_openness"))
74 128 assert claims_of(facts, some.name)["aa_openness"] in ("open-weights", "proprietary")
75 129 assert not any(c.property in ("openness", "context_length", "status", "release_date") for c in facts.claims) # second-hand facts stay aa_*
130 + aa_only = {e.name for e in facts.entities if "artificial_analysis_creator" in e.identifiers}
131 + assert not aa_only & {"Upstage", "StepFun", "Thinking Machines", "Kwaipilot"} # registry organisations now, not AA-only companies
132 + assert all("registry_org" in e.identifiers for e in facts.entities if e.name in ("Upstage", "StepFun", "Thinking Machines"))
133 + qwen = next(r for r in idx if r.config["aa_slug"] == "qwen3-4b-instruct-reasoning")
134 + assert qwen.model.name == "Qwen3 4B" and qwen.model.organization.name == "Qwen" and qwen.config["reasoning"] == "on"
135 +
136 +
137 +def test_identity_helper():
138 + m = model_identity("anthropic/claude-3-7-sonnet-20250219", trusted=True)
139 + assert m.identifiers == {"anthropic_model_id": "claude-3-7-sonnet-20250219"} and m.org_key == "anthropic" and m.pinned is True
140 + assert model_identity("anthropic/claude-3-7-sonnet-20250219").identifiers == {} # untrusted → aliases only
141 + m = model_identity("openrouter/x-ai/grok-4", trusted=True)
142 + assert m.identifiers == {"openrouter": "x-ai/grok-4"} and m.org_key == "xai" and "grok-4" in m.aliases
143 + m = model_identity("openai/Qwen/Qwen2.5-Coder-32B-Instruct", trusted=True)
144 + assert m.identifiers == {"hf_repo": "Qwen/Qwen2.5-Coder-32B-Instruct"} and m.org_key == "qwen" # `openai/` = OpenAI-compatible endpoint
145 + assert model_identity("openai/command-a-03-2025-quality").org_key == "cohere"
146 + assert model_identity("fireworks_ai/accounts/fireworks/models/qwq-32b", trusted=True).identifiers == {"fireworks_model_id": "fireworks/qwq-32b"}
147 + assert model_identity("yi-lightning").org_key is None and model_identity("r1").org_key is None
148 + assert strip_effort("claude-opus-4-5-20251101-thinking-64k-high-effort") == ("claude-opus-4-5-20251101", {"reasoning": "on", "thinking_budget": "64k", "reasoning_effort": "high"})
149 + assert strip_effort("mistral-medium") == ("mistral-medium", {}) and strip_effort("sonar-reasoning") == ("sonar-reasoning", {})
150 + assert strip_effort("kimi-k2-thinking") == ("kimi-k2-thinking", {}) and strip_effort("kimi-k2.6-thinking") == ("kimi-k2.6", {"reasoning": "on"})
151 + assert strip_effort("qwen3.7-max") == ("qwen3.7-max", {}) and strip_effort("grok-4-1-fast") == ("grok-4-1-fast", {}) # model tiers, not efforts
152 + assert split_effort_label("Claude Opus 5 (xhigh)") == ("Claude Opus 5", {"reasoning_effort": "xhigh"})
153 + assert split_effort_label("GPT-5.6 Sol xHigh Effort") == ("GPT-5.6 Sol", {"reasoning_effort": "xhigh"})
154 + assert split_effort_label("Kimi K2 Thinking") == ("Kimi K2 Thinking", {}) and split_effort_label("Mistral Large 2 (Jul)") == ("Mistral Large 2 (Jul)", {})
155 + assert split_effort_label("DeepSeek V3 (0324)") == ("DeepSeek V3 (0324)", {})
156 + assert split_effort_label("Mistral Small 4 (Non-reasoning)") == ("Mistral Small 4", {"reasoning": "off"})
157 + assert split_effort_label("gemini-2.5-pro-preview-06-05 (32k think)") == ("gemini-2.5-pro-preview-06-05", {"reasoning": "on", "thinking_budget": "32k"})
modified tests/test_mistral.py +12 −5
@@ -15,9 +15,15 @@ async def test_models_overview(connector):
15 15 models = entity_names(facts, "model")
16 16 assert len(models) >= 55
17 17 medium = claims_of(facts, "Mistral Medium 3.5")
18 − assert medium["license"] == "Modified MIT" and medium["openness"] == "open-weights" and medium["version"] == "26.04"
19 − assert medium["official_url"] == f"{DOCS}/models/mistral-medium-3-5-26-04"
20 − assert claims_of(facts, "Mistral Small 4")["license"] == "Apache 2.0"
18 + assert medium["license"] == "MIT-Modified" and medium["license_raw"] == "Modified MIT" and medium["openness"] == "open-weights" and medium["version"] == "26.04"
19 + assert medium["weights_available"] is True and medium["official_url"] == f"{DOCS}/models/mistral-medium-3-5-26-04"
20 + small = claims_of(facts, "Mistral Small 4")
21 + assert small["license"] == "Apache-2.0" and small["license_raw"] == "Apache 2.0" and small["openness"] == "open-weights" # badge → ontology key
22 + mrl = next((claims_of(facts, e.name) for e in facts.entities if e.entity_type == "model" and claims_of(facts, e.name).get("license_raw") == "MRL"), None)
23 + if mrl is not None:
24 + assert mrl["license"] == "Mistral-Research" and mrl["openness"] == "restricted-weights" # research licence ≠ open weights
25 + medium_ref = next(e for e in facts.entities if e.name == "Mistral Medium 3.5")
26 + assert medium_ref.family is not None and medium_ref.family.name == "Mistral" and medium_ref.family.organization.name == "Mistral AI"
21 27 # deprecation table: versioned API ids, dates (US format), status vs. today, alternative
22 28 m31 = next(e for e in facts.entities if e.name == "Mistral Medium 3.1")
23 29 assert m31.identifiers == {"mistral_docs_slug": "mistral-medium-3-1-25-08", "mistral_model_id": "mistral-medium-2508"}
@@ -42,8 +48,9 @@ async def test_model_page(connector):
42 48 target = Target(url=f"{DOCS}/models/mistral-medium-3-5-26-04", doc_type="model_page", key="model:mistral-medium-3-5-26-04", meta={"slug": "mistral-medium-3-5-26-04"})
43 49 facts = await extract_from_fixture(connector, target, fixture_path("mistral", "model-mistral-medium-3-5.html"))
44 50 m = claims_of(facts, "Mistral Medium 3.5")
45 − assert m["context_length"] == 256_000 and m["license"] == "Modified MIT" and m["openness"] == "open-weights"
46 − assert m["api_alias"] == "mistral-medium-latest" and m["tool_calling"] is True and m["structured_output"] is True
51 + assert m["context_length"] == 256_000 and m["license"] == "MIT-Modified" and m["license_raw"] == "Modified MIT" and m["openness"] == "open-weights"
52 + assert m["api_alias"] == "mistral-medium-latest" and m["api_aliases"] == ["mistral-medium-latest"] and m["tool_calling"] is True and m["structured_output"] is True
53 + assert {"function_calling", "structured_output"} <= set(m["capabilities"])
47 54 assert len(facts.prices) == 1
48 55 p = facts.prices[0]
49 56 assert (p.input_per_mtok, p.output_per_mtok) == (1.5, 7.5) and p.features["eur_input_per_mtok"] == 1.25 and p.provider.identifiers["registry_provider"] == "mistral"
modified tests/test_openai.py +4 −1
@@ -34,11 +34,14 @@ async def test_model_page(connector):
34 34 facts = await extract_from_fixture(connector, target, fixture_path("openai", "model-gpt-4.1.md"), content_type=MD)
35 35 m = claims_of(facts, "GPT-4.1")
36 36 assert m["context_length"] == 1_047_576 and m["max_output_tokens"] == 32_768 and m["knowledge_cutoff"] == "2024-06"
37 − assert m["modalities_input"] == ["text", "image"] and m["modalities_output"] == ["text"] and m["vision"] is True
37 + assert m["modalities_input"] == ["image", "text"] and m["modalities_output"] == ["text"] and m["vision"] is True # canonical, sorted
38 38 assert m["tool_calling"] is True and m["structured_output"] is True and m["fine_tuning_available"] is True
39 + assert {"function_calling", "structured_output", "fine_tuning"} <= set(m["capabilities"]) and "structured_outputs" not in m["capabilities"]
40 + assert "structured_outputs" in m["capabilities_raw"] # OpenAI's own slugs kept as raw
39 41 assert m["default_snapshot"] == "gpt-4.1-2025-04-14"
40 42 ref = next(e for e in facts.entities if e.entity_type == "model")
41 43 assert ref.identifiers == {"openai_model_id": "gpt-4.1"} and "gpt-4.1-2025-04-14" in ref.aliases
44 + assert ref.family is not None and ref.family.name == "GPT 4.1" and ref.family.organization.name == "OpenAI" and ref.identity_confidence == "high"
42 45 assert facts.prices == [] # provider prices come from the pricing page only
43 46
44 47
modified tests/test_openreview.py +8 −1
@@ -24,6 +24,13 @@ async def test_notes_shape():
24 24 assert a["venue"] == "ICLR 2026 Poster" and a["venue_id"] == "ICLR.cc/2026/Conference" and a["keywords"] == ["scaling laws", "fixtures"]
25 25 assert a["pdf_url"] == "https://openreview.net/pdf/synthetic-a.pdf" and a["official_url"] == "https://openreview.net/forum?id=synthAbc123"
26 26 assert a["published_at"] == "2025-09-20T00:00:00+00:00" and a["updated_at"].startswith("2025-09-21")
27 − assert len([r for r in facts.relations if r.predicate == "authored"]) == 3 + 1
27 + # researchers only from OpenReview profile ids (`~Name1`), never from bare names
28 + people = [e for e in facts.entities if e.entity_type == "researcher"]
29 + assert people and all(e.identifiers.get("openreview_profile", "").startswith("~") for e in people)
30 + ada = next(e for e in people if e.identifiers["openreview_profile"] == "~Ada_Placeholder1")
31 + assert ada.name == "Ada Placeholder" and ada.identity_confidence == "high"
32 + authored = [r for r in facts.relations if r.predicate == "authored"]
33 + assert len(authored) == len(people) and all(r.subject.identifiers.get("openreview_profile") for r in authored)
34 + assert claims_of(facts, "Ada Placeholder")["openreview_profile_url"] == "https://openreview.net/profile?id=~Ada_Placeholder1"
28 35 b = claims_of(facts, "Synthetic Example Paper B")
29 36 assert b["venue"] == "ICLR.cc/2026/Conference" and b["pdf_url"] == "https://openreview.net/pdf?id=synthDef456" and b["published_at"].startswith("2025-09-18")
modified tests/test_openrouter.py +13 −4
@@ -14,18 +14,27 @@ async def test_catalogue():
14 14 assert len(facts.prices) >= 150
15 15 sonnet = next(p for p in facts.prices if p.provider_model_id == "anthropic/claude-sonnet-4.5")
16 16 assert sonnet.model.name == "Claude Sonnet 4.5" and sonnet.model.organization.name == "Anthropic"
17 − assert sonnet.provider.name == "Anthropic API" and sonnet.features["via"] == "openrouter"
17 + # routed prices are OpenRouter's rows: booked on the aggregator only, the routed lab is a feature (never a second "current" price on its own provider)
18 + assert sonnet.provider.name == "OpenRouter" and sonnet.features["via"] == "openrouter" and sonnet.features["upstream_provider"] == "anthropic"
19 + assert sonnet.features["upstream_vendor"] == "anthropic"
18 20 assert (sonnet.input_per_mtok, sonnet.output_per_mtok, sonnet.cached_input_per_mtok) == (3.0, 15.0, 0.3) # per-token strings × 1e6
21 + assert sonnet.model.identifiers == {"openrouter": "anthropic/claude-sonnet-4.5"} and "claude-sonnet-4.5" in sonnet.model.aliases
22 + assert "anthropic_model_id" not in sonnet.model.identifiers # OpenRouter slugs ≠ vendor API ids
23 + assert sonnet.model.family is not None and sonnet.model.family.name == "Claude" and sonnet.model.identity_confidence == "medium"
19 24 claims = claims_of(facts, "Claude Sonnet 4.5")
20 − assert claims["context_length"] == 1_000_000 and claims["max_output_tokens"] == 64000 and claims["release_date"] == "2025-09-29"
21 − assert claims["modalities_input"] == ["image", "text"] and claims["file_input"] is True # "file" is not in the modality vocabulary
25 + assert claims["context_length"] == 1_000_000 and claims["max_output_tokens"] == 64000
26 + assert claims["openrouter_listed_at"] == "2025-09-29" and "release_date" not in claims # listing date is not a release date
27 + assert claims["modalities_input"] == ["document", "image", "text"] and claims["file_input"] is True # "file" → document (ontology)
22 28 assert "tools" in claims["supported_parameters"] and claims["tool_calling"] is True
23 29 by_provider = Counter(p.provider.name for p in facts.prices)
24 − assert by_provider["OpenAI API"] >= 50 and by_provider["OpenRouter"] >= 50 # unknown vendors → aggregator
30 + assert set(by_provider) == {"OpenRouter"} and by_provider["OpenRouter"] >= 150 # every routed price belongs to OpenRouter
31 + upstream = Counter(p.features.get("upstream_provider") for p in facts.prices)
32 + assert upstream["openai"] >= 50 and upstream[None] >= 50 # unknown vendors carry no upstream provider
25 33 variant = next(p for p in facts.prices if ":" in (p.provider_model_id or ""))
26 34 assert "variant" in variant.features and ":" not in variant.model.identifiers["openrouter"] # variants share the model identity
27 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
28 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)
29 38
30 39
31 40 def test_helpers():
modified tests/test_provider_pricing.py +6 −2
@@ -15,8 +15,10 @@ async def test_groq_models_table():
15 15 assert (p.input_per_mtok, p.output_per_mtok, p.context_length, p.max_output_tokens) == (0.15, 0.60, 131072, 65536)
16 16 assert p.provider.name == "GroqCloud" and p.model.name == "GPT OSS 120B" and p.model.organization.name == "OpenAI"
17 17 assert p.features["output_tokens_per_second"] == 500 and p.features["groq_section"] == "production"
18 − assert p.model.identifiers == {"groq_model_id": "openai/gpt-oss-120b"}
18 + assert p.model.identifiers == {"groq_model_id": "openai/gpt-oss-120b"} and p.model.family.name == "gpt-oss"
19 19 llama = claims_of(facts, "Llama 3.1 8B")
20 + llama_ref = next(e for e in facts.entities if e.name == "Llama 3.1 8B")
21 + assert llama_ref.organization.name == "Meta AI" # first-party pattern on the id, not a guess
20 22 assert "context_length" not in llama and "groq_pricing_note" in llama # "Contact Sales" → no price row; limits stay on price rows
21 23 assert p.features["context_length"] == 131072
22 24 assert not any(p.provider_model_id == "whisper-large-v3" for p in facts.prices) # per-hour pricing is not per-token
@@ -52,7 +54,9 @@ async def test_fireworks_markdown():
52 54 k3 = by_id["fireworks/kimi-k3"]
53 55 assert (k3.input_per_mtok, k3.cached_input_per_mtok, k3.output_per_mtok) == (3.0, 0.30, 15.0)
54 56 assert k3.features["priority"] == {"input_per_mtok": 3.75, "cached_input_per_mtok": 0.375, "output_per_mtok": 18.75} and k3.features["reserved_throughput"] is True
55 − assert k3.model.organization.name == "Moonshot AI" and k3.model.identifiers == {"fireworks-ai_model_id": "fireworks/kimi-k3"}
57 + assert k3.model.organization.name == "Moonshot AI"
58 + assert k3.model.identifiers == {"fireworks_model_id": "fireworks/kimi-k3", "fireworks-ai_model_id": "fireworks/kimi-k3"} # canonical + legacy scheme
59 + assert k3.model.family is not None and k3.model.family.name == "Kimi" and k3.model.identity_confidence == "medium"
56 60 fast = by_id["fireworks/kimi-k3:fast"]
57 61 assert fast.input_per_mtok == 4.5 and fast.model is k3.model and fast.features["serving_path"] == "fast"
58 62 assert by_id["fireworks/gpt-oss-120b"].output_per_mtok == 0.60 and len(facts.prices) == 21
59 63