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>
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 | |