SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%

Canonical upgrade foundation: ontology package (licences, openness, taxonomy, model naming, benchmarks, anomalies), migration 0003, Facts hierarchy hints, implementation audit

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

12 changed files +1,624 −1

added apps/web/qa/_shot_tmp.mjs +12 −0
@@ -0,0 +1,12 @@
1 +import { chromium } from 'playwright';
2 +const b = await chromium.launch();
3 +for (const [w,h,tag] of [[1440,900,'desk'],[390,844,'mob']]) {
4 + const ctx = await b.newContext({ viewport: { width: w, height: h }, colorScheme: 'dark' });
5 + const p = await ctx.newPage();
6 + for (const path of ['/', '/models/claude-fable-5.1', '/benchmarks/gpqa']) {
7 + await p.goto('https://www.ai-atlas.co' + path, { waitUntil: 'networkidle', timeout: 60000 }).catch(()=>{});
8 + await p.screenshot({ path: `/tmp/aia-${tag}-${path.replace(/[^a-z0-9]+/gi,'_')||'home'}.png`, fullPage: false });
9 + }
10 + await ctx.close();
11 +}
12 +await b.close();
added docs/UPGRADE-2026-09-AUDIT.md +137 −0
@@ -0,0 +1,137 @@
1 +# AI Atlas — implementation audit for the canonical upgrade (2026-09-12)
2 +
3 +Written before the "temporal knowledge graph + intelligence terminal" upgrade. Numbers are from the production database
4 +(M2M32c, dump `aiatlas-prod.dump` taken 2026-09-11 23:47 EDT, 133 MB) and from reading every module of the repository.
5 +
6 +## 1. Existing architecture (keep)
7 +
8 +FastAPI + SQLAlchemy Core/asyncpg + Alembic (forward-only SQL) · Redis cache · APScheduler scheduler (`aia schedule`) running connectors
9 +**sequentially** · Postgres job queue (SKIP LOCKED) · LLM gateway → llm-api.io (deterministic first, LLM claims one tier lower) ·
10 +Next 16 App Router (server components, Tailwind v4 tokens, hand-rolled d3 SVG charts, Playwright QA borrowed from `~/Desktop/uqo-eval`).
11 +Deployment: `mld` manifest `deploy/ai-atlas.mld.json`, PM2 `ai-atlas-api :8321 / ai-atlas-scheduler / ai-atlas-web :8320`, MacLustr Tunnel.
12 +
13 +## 2. Existing schema (0001 + 0002)
14 +
15 +`entities` (type, canonical_name, slug, organization_id, attributes/provenance/quality/counts jsonb, first/last_seen, merged_into, search) ·
16 +`entity_aliases` · `entity_identifiers (scheme,value) unique` · `sources` · `connectors` · `connector_runs` · `connector_errors` ·
17 +`documents` · `snapshots (run_id, raw_path, text_path, structured, diff, parser_version)` · `claims (temporal: valid_from/valid_to, status
18 +current|superseded|conflicting|retracted, tier, confidence, extractor)` · `relations (live uniq on subject,predicate,object)` ·
19 +`change_events (observed_at, effective_at, importance, dedupe_key)` · `benchmark_results (config jsonb, dedupe_key, valid_to)` ·
20 +`prices (live uniq per model×provider×provider_model_id)` · `jobs` · `llm_jobs` · `review_queue` · `entity_embeddings` · `stats_snapshots` ·
21 +`page_views` · `api_keys` · `metric_definitions` · `domains`.
22 +
23 +Missing for this upgrade: run/batch id on facts, backfill/occurrence semantics on events, canonical hierarchy columns, benchmark
24 +comparability key and trust level, persisted resolution decisions, anomaly flags, admin audit log, quarantine.
25 +
26 +## 3. Entity types in the data (prod, live rows)
27 +
28 +researcher 1 899 · model 1 880 · paper 1 060 · dataset 146 · company 119 · library 67 · framework 66 · hardware 42 · organization 36 ·
29 +benchmark 27 · provider 27 · repository 10 · lab 8 · university 1 · tool 0 (public nav shows Tools with "0 total").
30 +`ids.ENTITY_TYPES` declares 33 types; `quantization`, `license`, `release`, `agent`, `runtime`, `tool` are declared but never produced.
31 +
32 +## 4. Claim architecture (keep)
33 +
34 +`FactWriter.write_claim`: no current → insert; same → confirm; different & source tier ≤ current → supersede + event for material
35 +properties; different & worse tier → `conflicting` + review item. Soft properties never event. `metric.*` never event. Prices and
36 +results append-only with close/open. **Loophole**: `same_source` lets an LLM claim (tier+1) supersede a deterministic claim from the same
37 +URL (`writer.py:141-145`). **Gap**: no `run_id` on facts; `EntityRef.first_seen_hint` never read (`resolution.py:131`).
38 +
39 +## 5. Connector architecture (keep)
40 +
41 +`BaseConnector.run()` → discover → fetch (conditional) → archive → snapshot (+diff) → parse → extract → `FactWriter` → follow-ups →
42 +optional `llm_extract` job. Breakage detection (`expected_min_records`, only on full re-extraction), circuit breaker, adaptive interval,
43 +reprocess-from-archive. **Bugs**: single-URL reprocess builds key-less targets → no-op (`connector.py:188`); `Fetcher` has **no SSRF guard**
44 +(no private-IP/scheme/redirect validation, follow-ups come from crawled content); rate limit read from class attr, not registry.
45 +
46 +## 6. Current model resolution
47 +
48 +`Resolver`: identifiers (exact scheme+value) → normalized alias within type (org-disambiguated) → slug → create. `normalize_alias`
49 +erases dots/hyphens/spaces (`Qwen3-8B` ≡ `Qwen 38B`). Identifier schemes are per source (`anthropic_model_id`, `gemini_model_id`,
50 +`fireworks-ai_model_id`…) so cross-source identity rests on aliases. Consequences measured in prod:
51 +
52 +* **399 models exist only through an `artificial_analysis` identifier**, 204 of them are *effort/thinking variants* of another model
53 + (`claude-fable-5-1-xhigh`, `gpt-6-astra-high`, `deepseek-v4-pro-0424-non-reasoning`) — evaluation configurations, not models.
54 +* **157 Hugging Face repos flagged `is_quantized`** (`quant_format` gguf 64, fp8 41, onnx 27, mlx 23, awq 2) plus ~150 conversions/re-packagings
55 + (`amd/…-MXFP4`, `zai-org/GLM-5-FP8`, `mlx-community/Kimi-K2.5`) are typed `model`; they dominate "largest models" (Kimi K2.5 appears 4×).
56 +* 72 models exist only through OpenRouter; OpenRouter `release_date` = listing date; routed prices booked on the lab's provider entity with
57 + a different `provider_model_id` → two concurrent "current" prices for one model×provider.
58 +* 19 exact normalized-name duplicates across slugs (`gemini-2.5-flash-lite` / `gemini-2-5-flash-lite`, `deepseek-v3-{2,3,4}`…); 80 pending
59 + `merge_candidate` items; 140 pending `conflict` items (status 34, modalities_input 30, context_length 26).
60 +* 1 899 researchers created **by name only** (first 3 arXiv authors, no identifier, no affiliation) — homonyms merge, variants split.
61 +* 70 models without organization; 853 without release date; 1 205 without parameter count; only 115 carry `family`.
62 +
63 +## 7. Current benchmark pipeline
64 +
65 +Four boards (Artificial Analysis 4 003 rows, LiveBench 456, SWE-bench 323, aider 138) → 4 920 live rows on 15 of 27 registered benchmarks
66 +(12 registry benchmarks — MMLU, HumanEval, AIME 2025, MATH-500, MTEB, LMArena… — have **zero** results: no connector feeds them).
67 +Leaderboards are one row per live result: LiveBench mixes `global_average` with 7 `category:*` metrics in one board; aider mixes
68 +`pass_rate_2` and `percent_cases_well_formed`; every LiveBench release / aider run date creates new rows that are never retired;
69 +`/compare` picks the last-observed metric per model. Models are resolved by free text (aider, SWE-bench) or AA slug, so AA effort variants
70 +become separate leaderboard entries (GPQA: 614 results = 614 "models"). No trust level, no comparability key, no metric bounds.
71 +The registry lacks family / variant / direction / metric range. The API only resolves benchmarks by exact slug (no aliases).
72 +
73 +## 8. Current event pipeline
74 +
75 +12 196 events, **all observed on 2026-09-11** (initial corpus): NEW_MODEL 1 880 (importance 3), BENCHMARK_RESULT 4 920, ANNOUNCEMENT 1 764,
76 +NEW_PAPER 1 060, RELEASE 692, PROVIDER_LISTED 613… `/changes`, `/changes/daily` and `/stats.change_events_24h` key on `observed_at`; only
77 +`/timeline` uses `coalesce(effective_at, observed_at)`. 8 713 events have no `effective_at`. There is no backfill flag, no
78 +occurred/observed/recorded distinction, no semantic dedupe of one release across sources. Homepage says "+12,196 · 24 h".
79 +
80 +## 9. Taxonomy problems found (prod values)
81 +
82 +* license: `apache-2.0` 344 · `Apache 2.0` 9 · `Modified MIT` · `CC BY-NC 4.0` vs `cc-by-nc-4.0` · `other` 157 · Llama variants `llama3`, `llama3.1`… (no SPDX, no permissions)
83 +* openness: `open-weights` 971 · empty 478 · `proprietary` 348 · `restricted` 82 (= HF *gated*, not a licence property) · `open-source` 1 (Cohere prose)
84 +* modalities: `["Text"]` vs `["text"]`; Google emits `pdf`; Anthropic values are hard-coded constants; `capabilities` uses OpenAI slugs and Google labels
85 +* status: `limited-availability` vs schema `available`; `archived` undocumented; `groq_status` side channel
86 +* org kinds: `company|organization|lab|university` as entity types, with duplicates (`Kwaipilot` company + `kwaipilot-2` organization, `Upstage`/`upstage-2`, `StepFun`/`stepfun-ai`, `Thinking Machines`/`thinkingmachines`)
87 +* identifiers: `gemini_model_id` (provider key is `google-gemini-api`), `fireworks-ai_model_id` (hyphen), platform ids stored as claims
88 +* hardware `kind`: `soc|gpu|system|computer`; `memory_gb` int vs list
89 +* benchmarks: `metric` free text (`pass rate (2 attempts)`), `unit` `%` or empty
90 +
91 +## 10. Duplicate / hierarchy problems
92 +
93 +Model = release = checkpoint = quantization = provider alias today. Needed hierarchy: **model_family → model → artifact → deployment**.
94 +Artifact kinds: `checkpoint` (official weights repo), `quantization`, `conversion` (BF16/FP8 repack, MLX, ONNX), `packaging`.
95 +Evaluation-effort variants fold into the canonical model as **result configuration** (`reasoning_effort`, `reasoning`), never entities.
96 +
97 +## 11. Components to preserve
98 +
99 +Archive (`sdk/archive.py`), `FactWriter` temporal rules, `Resolver` precedence, connector SDK and all 27 connectors + 91 fixture tests,
100 +LLM gateway cascade and accounting, `/asof` `/history` `/diff` `/compare` `/prices/index` `/hardware/fit` endpoints and their semantics,
101 +`merge_entities`, quality methodology, design tokens, `KeyValue`/`DataTable`/`Tabs`/`ProvenanceInline`, compare tray, admin console,
102 +sitemap shards, OG frame, slugs, ids, all public URLs.
103 +
104 +## 12. Schema migration 0003 (`0003_canonical_ontology`)
105 +
106 +* `entities`: `family_id`, `canonical_id` (artifact → model, folded variant → model), `artifact_kind`, `identity_confidence`; new types
107 + `model_family` (prefix `family`), `artifact` (prefix `artifact`); `license` entities created from the ontology (`uses_license` relation).
108 +* facts: `run_id` on `claims`, `relations`, `prices`, `benchmark_results`, `change_events` (+ indexes) for batch rollback.
109 +* `change_events`: `recorded_at`, `is_backfill`, generated `occurred_at = coalesce(effective_at, observed_at)`, `group_key` (semantic
110 + dedupe of one release across documents), indexes on `(is_backfill, occurred_at)`.
111 +* `benchmark_results`: `trust_level`, `config_key` (comparability), `variant`, `run_group`, `is_current` maintained by the writer
112 + (one current row per model × benchmark × metric × config_key; older runs closed with `valid_to`).
113 +* new tables: `resolution_decisions`, `anomalies`, `admin_audit_log`, `quarantined_runs`, `taxonomy_mappings`, `watch_digests` (none), `model_families` view.
114 +* benchmark registry gains `family, variant, version, direction, metric_min, metric_max, harness, comparability` fields (claims).
115 +
116 +## 13. Backward compatibility
117 +
118 +* Every existing slug and id keeps resolving: artifacts stay reachable at `/models/<slug>` (API accepts `artifact` on the models mount,
119 + web 301s to `/artifacts/<slug>`); folded effort variants resolve through `merged_into` (API follows it, web 301s to the canonical model).
120 +* `/models` excludes artifacts and merged rows by default (`include=artifacts` restores the old universe); documented in `docs/API.md`
121 + as v1.1 with a changelog. No v2 needed: no field is removed or retyped; new fields are additive.
122 +* Raw snapshots are never modified; taxonomy normalisation writes canonical attributes and keeps `*_raw` when the source label differs.
123 +* Counters get explicit definitions ("Models = canonical model releases; artifacts and folded variants excluded").
124 +
125 +## 14. Implementation sequence
126 +
127 +1. Ontology package (`src/aiatlas/ontology/`) + migration 0003 + tests.
128 +2. SDK: writer/resolver use the ontology; `run_id` propagation; `first_seen_hint`; same-source loophole; SSRF guard; reprocess fix; quarantine.
129 +3. `aia canonicalize` (dry-run + apply): artifacts, effort variants, families, licences, taxonomy, org duplicates, event backfill flags,
130 + benchmark result comparability/trust; `aia anomalies`; `aia audit-data`.
131 +4. Connectors: HF → artifacts, AA → configs, leaderboards metrics split, benchmark registry families, OpenRouter fixes, provider ids.
132 +5. API 1.1: families/artifacts/deployments/frontier/pulse/pareto/finder/cost/matrix/history/comparability/time-machine/claims/admin quality
133 + & resolution & extraction debugger & audit; counters; backfill-aware feeds; search compiler v2; security fixes.
134 +6. Web: brand, shell (nav + ⌘K commands + evidence drawer + density), homepage 3.0, model page 3.0, compare 3.0, benchmarks 2.0,
135 + frontier, prices terminal, calculator, run-locally, find-a-model, open, graph, timeline 2.0, time machine, diff, pulse, families,
136 + watchlist, admin workbenches, mobile, OG per type, SEO.
137 +7. Tests, QA sweeps, final data/benchmark/event/counter audits, deploy, memory.
added migrations/versions/0003_canonical_ontology.py +188 −0
@@ -0,0 +1,188 @@
1 +"""Canonical ontology: model hierarchy columns (family / canonical / artifact kind), run ids on every fact for batch rollback,
2 +event semantics (recorded_at, occurred_at, is_backfill, group_key), benchmark comparability (config_key, trust_level, variant,
3 +run_group, is_current), persisted entity-resolution decisions, anomaly flags, admin audit log, quarantined runs, taxonomy mappings.
4 +
5 +Additive only — no column is dropped or retyped; existing rows keep their values.
6 +
7 +Revision ID: 0003
8 +"""
9 +from __future__ import annotations
10 +
11 +from alembic import op
12 +
13 +revision = "0003"
14 +down_revision = "0002"
15 +branch_labels = None
16 +depends_on = None
17 +
18 +SQL = r"""
19 +-- ---------------------------------------------------------------------------------------------- entity hierarchy
20 +alter table entities add column if not exists family_id text references entities(id);
21 +-- artifact → model; folded variant → model
22 +alter table entities add column if not exists canonical_id text references entities(id);
23 +-- checkpoint|quantization|conversion|packaging (artifacts only)
24 +alter table entities add column if not exists artifact_kind text;
25 +-- high|medium|low (how sure we are this row is one real thing)
26 +alter table entities add column if not exists identity_confidence text not null default 'high';
27 +create index if not exists entities_family_idx on entities (family_id) where family_id is not null;
28 +create index if not exists entities_canonical_idx on entities (canonical_id) where canonical_id is not null;
29 +create index if not exists entities_type_slug_idx on entities (entity_type, slug);
30 +create index if not exists entities_type_status_idx on entities (entity_type, status) where merged_into is null;
31 +
32 +-- ---------------------------------------------------------------------------------------------- run ids on facts (batch rollback)
33 +alter table claims add column if not exists run_id text;
34 +alter table relations add column if not exists run_id text;
35 +alter table prices add column if not exists run_id text;
36 +alter table benchmark_results add column if not exists run_id text;
37 +alter table change_events add column if not exists run_id text;
38 +create index if not exists claims_run_idx on claims (run_id) where run_id is not null;
39 +create index if not exists relations_run_idx on relations (run_id) where run_id is not null;
40 +create index if not exists prices_run_idx on prices (run_id) where run_id is not null;
41 +create index if not exists benchmark_results_run_idx on benchmark_results (run_id) where run_id is not null;
42 +create index if not exists change_events_run_idx on change_events (run_id) where run_id is not null;
43 +-- source label when the canonical value differs
44 +alter table claims add column if not exists value_raw text;
45 +
46 +-- ---------------------------------------------------------------------------------------------- event semantics
47 +alter table change_events add column if not exists recorded_at timestamptz not null default now();
48 +alter table change_events add column if not exists is_backfill boolean not null default false;
49 +-- semantic group (one release across documents)
50 +alter table change_events add column if not exists group_key text;
51 +alter table change_events add column if not exists occurred_at timestamptz generated always as (coalesce(effective_at, observed_at)) stored;
52 +create index if not exists change_events_live_idx on change_events (occurred_at desc) where is_backfill = false and event_type <> 'DOCUMENT_CHANGED';
53 +create index if not exists change_events_occurred_idx on change_events (occurred_at desc);
54 +create index if not exists change_events_group_idx on change_events (group_key) where group_key is not null;
55 +create index if not exists change_events_live_importance_idx on change_events (importance desc, occurred_at desc) where is_backfill = false;
56 +
57 +-- ---------------------------------------------------------------------------------------------- benchmark comparability & trust
58 +alter table benchmark_results add column if not exists config_key text;
59 +alter table benchmark_results add column if not exists trust_level text;
60 +alter table benchmark_results add column if not exists variant text;
61 +alter table benchmark_results add column if not exists run_group text;
62 +alter table benchmark_results add column if not exists is_current boolean not null default true;
63 +alter table benchmark_results add column if not exists extractor text not null default 'deterministic';
64 +create index if not exists benchmark_results_current2_idx on benchmark_results (benchmark_id, config_key, is_current) where valid_to is null;
65 +create index if not exists benchmark_results_model_metric_idx on benchmark_results (model_id, benchmark_id, metric) where valid_to is null;
66 +
67 +-- ---------------------------------------------------------------------------------------------- persisted resolution decisions
68 +create table if not exists resolution_decisions (
69 + id text primary key,
70 + a_id text not null references entities(id) on delete cascade,
71 + b_id text not null references entities(id) on delete cascade,
72 + decision text not null, -- merge|alias|variant_of|family_member|keep_separate|defer
73 + actor text not null default 'admin',
74 + note text,
75 + payload jsonb not null default '{}'::jsonb,
76 + applied boolean not null default false,
77 + created_at timestamptz not null default now(),
78 + unique (a_id, b_id, decision)
79 +);
80 +create index if not exists resolution_decisions_pair_idx on resolution_decisions (a_id, b_id);
81 +create index if not exists resolution_decisions_b_idx on resolution_decisions (b_id);
82 +
83 +-- ---------------------------------------------------------------------------------------------- anomaly flags (never deletes)
84 +create table if not exists anomalies (
85 + id text primary key,
86 + entity_id text references entities(id) on delete cascade,
87 + check_name text not null,
88 + severity text not null, -- critical|warning|info
89 + message text not null,
90 + value jsonb,
91 + detail jsonb not null default '{}'::jsonb,
92 + status text not null default 'open', -- open|resolved|ignored|fixed
93 + first_seen_at timestamptz not null default now(),
94 + last_seen_at timestamptz not null default now(),
95 + resolved_at timestamptz,
96 + resolution text,
97 + dedupe_key text unique
98 +);
99 +create index if not exists anomalies_status_idx on anomalies (status, severity, last_seen_at desc);
100 +create index if not exists anomalies_entity_idx on anomalies (entity_id);
101 +
102 +-- ---------------------------------------------------------------------------------------------- admin audit log
103 +create table if not exists admin_audit_log (
104 + id bigserial primary key,
105 + actor text not null default 'admin',
106 + action text not null,
107 + target text,
108 + payload jsonb not null default '{}'::jsonb,
109 + ip text,
110 + created_at timestamptz not null default now()
111 +);
112 +create index if not exists admin_audit_log_created_idx on admin_audit_log (created_at desc);
113 +
114 +-- ---------------------------------------------------------------------------------------------- quarantined runs (held facts awaiting review)
115 +create table if not exists quarantined_runs (
116 + id text primary key,
117 + run_id text,
118 + connector_name text not null,
119 + reason text not null,
120 + stats jsonb not null default '{}'::jsonb, -- baseline vs observed counts
121 + facts jsonb not null default '[]'::jsonb, -- serialised Facts objects, written on release
122 + status text not null default 'pending', -- pending|released|discarded
123 + created_at timestamptz not null default now(),
124 + resolved_at timestamptz,
125 + resolved_by text
126 +);
127 +create index if not exists quarantined_runs_status_idx on quarantined_runs (status, created_at desc);
128 +
129 +-- ---------------------------------------------------------------------------------------------- taxonomy mappings observed (raw → canonical)
130 +create table if not exists taxonomy_mappings (
131 + domain text not null, -- license|openness|modality|status|org_kind|hardware_kind|framework_kind|metric
132 + raw text not null,
133 + canonical text,
134 + count integer not null default 1,
135 + first_seen_at timestamptz not null default now(),
136 + last_seen_at timestamptz not null default now(),
137 + primary key (domain, raw)
138 +);
139 +
140 +-- ---------------------------------------------------------------------------------------------- connector run baselines (anomaly detector)
141 +alter table connector_runs add column if not exists quarantined boolean not null default false;
142 +alter table connector_runs add column if not exists baseline jsonb;
143 +-- rolling medians of records/prices/results
144 +alter table connectors add column if not exists baseline jsonb;
145 +
146 +-- ---------------------------------------------------------------------------------------------- search vector: include aliases of family
147 +create or replace function entities_search_update() returns trigger language plpgsql as $$
148 +begin
149 + new.search :=
150 + setweight(to_tsvector('simple', coalesce(new.canonical_name, '')), 'A') ||
151 + setweight(to_tsvector('simple', coalesce(new.attributes->>'family', '')), 'B') ||
152 + setweight(to_tsvector('simple', coalesce(new.attributes->>'api_model_id', '')), 'B') ||
153 + setweight(to_tsvector('simple', coalesce(new.entity_type, '')), 'C') ||
154 + setweight(to_tsvector('english', left(coalesce(new.description, ''), 4000)), 'C');
155 + new.updated_at := now();
156 + return new;
157 +end $$;
158 +"""
159 +
160 +
161 +def upgrade() -> None:
162 + for statement in _split(SQL):
163 + op.execute(statement)
164 +
165 +
166 +def downgrade() -> None:
167 + raise RuntimeError("AI Atlas migrations are forward-only: historical data is never disposable")
168 +
169 +
170 +def _split(sql: str) -> list[str]:
171 + out: list[str] = []
172 + buf: list[str] = []
173 + in_dollar = False
174 + for line in sql.splitlines():
175 + stripped = line.strip()
176 + if stripped.count("$$") % 2 == 1:
177 + in_dollar = not in_dollar
178 + buf.append(line)
179 + if not in_dollar and stripped.endswith(";"):
180 + body = [ln for ln in buf if not ln.strip().startswith("--") or in_dollar]
181 + stmt = "\n".join(body).strip()
182 + if stmt:
183 + out.append(stmt)
184 + buf = []
185 + tail = "\n".join(ln for ln in buf if not ln.strip().startswith("--")).strip()
186 + if tail:
187 + out.append(tail)
188 + return out
modified src/aiatlas/ids.py +2 −0
@@ -8,6 +8,8 @@ from ulid import ULID
8 8
9 9 PREFIXES: dict[str, str] = {
10 10 "model": "model",
11 + "model_family": "family", # Llama 4, Qwen3.6, Claude — groups model releases
12 + "artifact": "artifact", # checkpoint / quantisation / conversion of a model (never a model of its own)
11 13 "company": "company",
12 14 "organization": "org",
13 15 "researcher": "person",
added src/aiatlas/ontology/__init__.py +50 −0
@@ -0,0 +1,50 @@
1 +"""AI Atlas ontology — canonical vocabularies and deterministic normalisers.
2 +
3 +Everything here is pure Python (no database, no network): connectors, the writer, the API and the QA checks all import the same
4 +definitions so that "Apache 2.0", "apache-2.0" and "Apache-2.0" are one licence, "Text" and "text" one modality, and
5 +`unsloth/Qwen3.6-35B-A3B-GGUF` an *artifact* of the model `Qwen3.6-35B-A3B` rather than a new foundation model.
6 +
7 +Modules
8 + licenses SPDX-anchored licence ontology (permissions, restrictions, category) + `normalize_license`
9 + openness openness dimensions → carefully labelled category + `normalize_openness`
10 + taxonomy small enums (modalities, statuses, organization kinds, hardware kinds, framework kinds) + normalisers
11 + models model naming: quantisation/precision detection, evaluation-effort variants, family inference, base-name keys
12 + benchmarks benchmark families / variants / metric ranges + comparability of two result configurations
13 + anomalies deterministic sanity checks on entity attributes, prices and results (flags, never deletes)
14 +"""
15 +from aiatlas.ontology.licenses import LICENSES, LicenseInfo, normalize_license
16 +from aiatlas.ontology.openness import OPENNESS_CATEGORIES, derive_openness, normalize_openness, openness_dimensions
17 +from aiatlas.ontology.taxonomy import (
18 + FRAMEWORK_KINDS,
19 + HARDWARE_KINDS,
20 + MODALITIES,
21 + MODEL_STATUSES,
22 + ORG_KINDS,
23 + normalize_framework_kind,
24 + normalize_hardware_kind,
25 + normalize_modalities,
26 + normalize_modality,
27 + normalize_org_kind,
28 + normalize_status,
29 +)
30 +
31 +__all__ = [
32 + "FRAMEWORK_KINDS",
33 + "HARDWARE_KINDS",
34 + "LICENSES",
35 + "MODALITIES",
36 + "MODEL_STATUSES",
37 + "OPENNESS_CATEGORIES",
38 + "ORG_KINDS",
39 + "LicenseInfo",
40 + "derive_openness",
41 + "normalize_framework_kind",
42 + "normalize_hardware_kind",
43 + "normalize_license",
44 + "normalize_modalities",
45 + "normalize_modality",
46 + "normalize_openness",
47 + "normalize_org_kind",
48 + "normalize_status",
49 + "openness_dimensions",
50 +]
added src/aiatlas/ontology/anomalies.py +178 −0
@@ -0,0 +1,178 @@
1 +"""Deterministic sanity checks. Each check returns `Anomaly` records — flags with evidence, never deletions or silent fixes.
2 +
3 +Severity: `critical` (value is physically impossible or contradicts itself), `warning` (implausible, needs a look), `info` (worth
4 +knowing, e.g. a duplicate candidate). `dedupe_key` keeps one open row per (entity, check)."""
5 +from __future__ import annotations
6 +
7 +from dataclasses import dataclass, field
8 +from datetime import UTC, date, datetime
9 +from typing import Any
10 +
11 +from aiatlas.ontology.benchmarks import metric_bounds
12 +
13 +MAX_PARAMS = 10e12 # 10T
14 +MAX_CONTEXT = 100_000_000 # 100M tokens
15 +MAX_OUTPUT = 100_000_000
16 +MAX_PRICE_PER_MTOK = 5_000.0 # USD per 1M tokens
17 +MAX_MEMORY_GB = 100_000
18 +MIN_RELEASE = date(2010, 1, 1)
19 +
20 +
21 +@dataclass
22 +class Anomaly:
23 + check: str
24 + severity: str
25 + message: str
26 + entity_id: str | None = None
27 + value: Any = None
28 + detail: dict[str, Any] = field(default_factory=dict)
29 +
30 + @property
31 + def dedupe_key(self) -> str:
32 + return f"{self.check}:{self.entity_id or self.detail.get('key', '')}"
33 +
34 +
35 +def _num(v: Any) -> float | None:
36 + if isinstance(v, bool):
37 + return None
38 + if isinstance(v, (int, float)):
39 + return float(v)
40 + if isinstance(v, str):
41 + try:
42 + return float(v.replace(",", ""))
43 + except ValueError:
44 + return None
45 + return None
46 +
47 +
48 +def _date(v: Any) -> date | None:
49 + if isinstance(v, datetime):
50 + return v.date()
51 + if isinstance(v, date):
52 + return v
53 + if isinstance(v, str) and len(v) >= 4 and v[:4].isdigit():
54 + try:
55 + parts = v[:10].split("-")
56 + y = int(parts[0]); m = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 1; d = int(parts[2]) if len(parts) > 2 and parts[2][:2].isdigit() else 1
57 + return date(y, max(1, min(12, m)), max(1, min(28, d)))
58 + except ValueError:
59 + return None
60 + return None
61 +
62 +
63 +def check_model(entity_id: str, name: str, attrs: dict[str, Any], *, today: date | None = None) -> list[Anomaly]:
64 + today = today or datetime.now(UTC).date()
65 + out: list[Anomaly] = []
66 + params = _num(attrs.get("parameter_count"))
67 + active = _num(attrs.get("active_parameter_count"))
68 + ctx = _num(attrs.get("context_length"))
69 + max_out = _num(attrs.get("max_output_tokens"))
70 + rel = _date(attrs.get("release_date"))
71 + dep = _date(attrs.get("deprecation_date"))
72 + ret = _date(attrs.get("retirement_date"))
73 + cutoff = _date(attrs.get("knowledge_cutoff"))
74 +
75 + if params is not None and params > MAX_PARAMS:
76 + out.append(Anomaly("params_too_large", "critical", f"{name}: parameter_count {params:.3g} exceeds {MAX_PARAMS:.0e}", entity_id, params))
77 + if params is not None and params < 1e5:
78 + out.append(Anomaly("params_too_small", "warning", f"{name}: parameter_count {params:.3g} below 100K", entity_id, params))
79 + if params is not None and active is not None and active > params:
80 + out.append(Anomaly("active_gt_total", "critical", f"{name}: active parameters {active:.3g} > total {params:.3g}", entity_id, active,
81 + {"parameter_count": params, "active_parameter_count": active}))
82 + if ctx is not None and ctx > MAX_CONTEXT:
83 + out.append(Anomaly("context_too_large", "critical", f"{name}: context_length {ctx:.0f} exceeds 100M tokens", entity_id, ctx))
84 + if ctx is not None and ctx < 256:
85 + out.append(Anomaly("context_too_small", "warning", f"{name}: context_length {ctx:.0f} below 256 tokens", entity_id, ctx))
86 + if max_out is not None and ctx is not None and max_out > ctx:
87 + out.append(Anomaly("max_output_gt_context", "warning", f"{name}: max_output_tokens {max_out:.0f} > context_length {ctx:.0f}", entity_id, max_out,
88 + {"context_length": ctx}))
89 + if rel and rel > today:
90 + out.append(Anomaly("release_in_future", "critical", f"{name}: release_date {rel.isoformat()} is in the future", entity_id, rel.isoformat()))
91 + if rel and rel < MIN_RELEASE:
92 + out.append(Anomaly("release_too_old", "warning", f"{name}: release_date {rel.isoformat()} before 2010", entity_id, rel.isoformat()))
93 + if rel and dep and dep < rel:
94 + out.append(Anomaly("deprecated_before_release", "critical", f"{name}: deprecation_date {dep} before release_date {rel}", entity_id, dep.isoformat(),
95 + {"release_date": rel.isoformat()}))
96 + if rel and ret and ret < rel:
97 + out.append(Anomaly("retired_before_release", "critical", f"{name}: retirement_date {ret} before release_date {rel}", entity_id, ret.isoformat(),
98 + {"release_date": rel.isoformat()}))
99 + if dep and ret and ret < dep:
100 + out.append(Anomaly("retired_before_deprecated", "warning", f"{name}: retirement_date {ret} before deprecation_date {dep}", entity_id, ret.isoformat()))
101 + if cutoff and rel and cutoff > rel:
102 + out.append(Anomaly("cutoff_after_release", "warning", f"{name}: knowledge_cutoff {cutoff} after release_date {rel}", entity_id, cutoff.isoformat()))
103 + status = str(attrs.get("status") or "").lower()
104 + if status in ("deprecated", "retired") and rel and rel > today:
105 + out.append(Anomaly("deprecated_future_release", "critical", f"{name}: status {status} but release in the future", entity_id, status))
106 + if attrs.get("openness") in ("open-weights", "open-source") and not any(attrs.get(k) for k in ("hf_repo", "repository_url", "model_card_url", "weights_url")):
107 + out.append(Anomaly("open_without_weights_url", "info", f"{name}: labelled {attrs.get('openness')} but no weights location recorded", entity_id, attrs.get("openness")))
108 + return out
109 +
110 +
111 +def check_price(row: dict[str, Any]) -> list[Anomaly]:
112 + out: list[Anomaly] = []
113 + key = f"{row.get('model_id')}:{row.get('provider_id')}:{row.get('provider_model_id') or ''}"
114 + label = f"{row.get('model_name') or row.get('model_id')} @ {row.get('provider_name') or row.get('provider_id')}"
115 + inp, outp = _num(row.get("input_per_mtok")), _num(row.get("output_per_mtok"))
116 + for k, v in (("input_per_mtok", inp), ("output_per_mtok", outp), ("cached_input_per_mtok", _num(row.get("cached_input_per_mtok"))),
117 + ("batch_input_per_mtok", _num(row.get("batch_input_per_mtok"))), ("batch_output_per_mtok", _num(row.get("batch_output_per_mtok")))):
118 + if v is not None and v < 0:
119 + out.append(Anomaly("negative_price", "critical", f"{label}: {k} is negative ({v})", row.get("model_id"), v, {"key": key, "field": k, "price_id": row.get("id")}))
120 + if v is not None and v > MAX_PRICE_PER_MTOK:
121 + out.append(Anomaly("price_too_high", "warning", f"{label}: {k} = ${v:g} per 1M tokens", row.get("model_id"), v, {"key": key, "field": k, "price_id": row.get("id")}))
122 + if inp is not None and outp is not None and outp == 0 and inp > 0 and not (row.get("features") or {}).get("free"):
123 + out.append(Anomaly("zero_output_price", "warning", f"{label}: output price is 0 while input is ${inp:g}", row.get("model_id"), 0, {"key": key, "price_id": row.get("id")}))
124 + if inp is not None and outp is not None and inp > 0 and outp > 0 and inp > outp * 4:
125 + out.append(Anomaly("input_gt_output_price", "info", f"{label}: input ${inp:g} is more than 4× output ${outp:g}", row.get("model_id"), inp, {"key": key, "price_id": row.get("id")}))
126 + cached = _num(row.get("cached_input_per_mtok"))
127 + if cached is not None and inp is not None and cached > inp:
128 + out.append(Anomaly("cached_gt_input_price", "warning", f"{label}: cached input ${cached:g} > input ${inp:g}", row.get("model_id"), cached, {"key": key, "price_id": row.get("id")}))
129 + return out
130 +
131 +
132 +def check_price_movement(old: dict[str, Any], new: dict[str, Any]) -> list[Anomaly]:
133 + out: list[Anomaly] = []
134 + for k in ("input_per_mtok", "output_per_mtok"):
135 + a, b = _num(old.get(k)), _num(new.get(k))
136 + if a and b and a > 0 and b > 0 and (b / a > 100 or a / b > 100):
137 + out.append(Anomaly("price_jump_100x", "critical", f"{k} moved {a:g} → {b:g} (>100×)", new.get("model_id"), b,
138 + {"key": f"{new.get('model_id')}:{new.get('provider_id')}:{k}", "old": a, "new": b}))
139 + return out
140 +
141 +
142 +def check_result(row: dict[str, Any]) -> list[Anomaly]:
143 + out: list[Anomaly] = []
144 + score = _num(row.get("score"))
145 + lo, hi = metric_bounds(row.get("metric"), row.get("unit"))
146 + label = f"{row.get('model_name') or row.get('model_id')} on {row.get('benchmark_name') or row.get('benchmark_id')}"
147 + if score is not None and hi is not None and score > hi + 1e-9:
148 + out.append(Anomaly("score_above_max", "critical", f"{label}: {score:g} > metric maximum {hi:g}", row.get("model_id"), score,
149 + {"key": row.get("id"), "result_id": row.get("id"), "metric": row.get("metric")}))
150 + if score is not None and lo is not None and score < lo - 1e-9:
151 + out.append(Anomaly("score_below_min", "critical", f"{label}: {score:g} < metric minimum {lo:g}", row.get("model_id"), score,
152 + {"key": row.get("id"), "result_id": row.get("id"), "metric": row.get("metric")}))
153 + ev = _date(row.get("evaluated_at"))
154 + rel = _date(row.get("model_release_date"))
155 + if ev and rel and ev < rel and (rel - ev).days > 45:
156 + out.append(Anomaly("evaluated_before_release", "warning", f"{label}: evaluated {ev} before model release {rel}", row.get("model_id"), ev.isoformat(),
157 + {"key": row.get("id"), "result_id": row.get("id")}))
158 + return out
159 +
160 +
161 +def check_hardware(entity_id: str, name: str, attrs: dict[str, Any]) -> list[Anomaly]:
162 + out: list[Anomaly] = []
163 + mem = attrs.get("memory_gb")
164 + mems = mem if isinstance(mem, list) else [mem]
165 + for m in mems:
166 + v = _num(m)
167 + if v is not None and (v <= 0 or v > MAX_MEMORY_GB):
168 + out.append(Anomaly("memory_implausible", "critical", f"{name}: memory_gb {v:g} implausible", entity_id, v))
169 + bw = _num(attrs.get("memory_bandwidth_gbs"))
170 + if bw is not None and (bw <= 0 or bw > 100_000):
171 + out.append(Anomaly("bandwidth_implausible", "warning", f"{name}: memory_bandwidth_gbs {bw:g} implausible", entity_id, bw))
172 + tdp = _num(attrs.get("tdp_watts"))
173 + if tdp is not None and (tdp <= 0 or tdp > 200_000):
174 + out.append(Anomaly("tdp_implausible", "warning", f"{name}: tdp_watts {tdp:g} implausible", entity_id, tdp))
175 + return out
176 +
177 +
178 +__all__ = ["Anomaly", "check_hardware", "check_model", "check_price", "check_price_movement", "check_result"]
added src/aiatlas/ontology/benchmarks.py +219 −0
@@ -0,0 +1,219 @@
1 +"""Benchmark ontology — families, variants, metric bounds, trust levels and the comparability of two result configurations.
2 +
3 +Two scores are only *directly comparable* when they measure the same benchmark variant with the same metric under configurations that
4 +do not change the task: same dataset revision/variant, same evaluator or harness class, same scaffold when the benchmark is agentic,
5 +same shot/pass regime. Reasoning effort, sampling temperature and judge differences make them *partially comparable*. Different
6 +variants (SWE-bench Verified vs Lite), different metrics, or different pass regimes are *not directly comparable*.
7 +"""
8 +from __future__ import annotations
9 +
10 +import hashlib
11 +import json
12 +import re
13 +from typing import Any
14 +
15 +COMPARABLE, PARTIAL, NOT_COMPARABLE = "comparable", "partially-comparable", "not-comparable"
16 +
17 +# Benchmark family → known variants (registry keys). Used to build `variant_of` relations and the family attribute; the registry YAML
18 +# carries the authoritative per-entry `family`/`variant` fields, this table is the fallback for benchmarks created from other sources.
19 +FAMILIES: dict[str, dict[str, Any]] = {
20 + "swe-bench": {"label": "SWE-bench", "variants": {"swe-bench-full": "full", "swe-bench-verified": "Verified", "swe-bench-lite": "Lite",
21 + "swe-bench-multimodal": "Multimodal", "swe-bench-multilingual": "Multilingual", "swe-bench-pro": "Pro"}},
22 + "aime": {"label": "AIME", "variants": {"aime-2024": "2024", "aime-2025": "2025", "aime-2026": "2026"}},
23 + "mmlu": {"label": "MMLU", "variants": {"mmlu": "original", "mmlu-pro": "Pro", "mmlu-redux": "Redux", "mmmlu": "multilingual"}},
24 + "mmmu": {"label": "MMMU", "variants": {"mmmu": "original", "mmmu-pro": "Pro"}},
25 + "gpqa": {"label": "GPQA", "variants": {"gpqa": "main", "gpqa-diamond": "Diamond"}},
26 + "tau-bench": {"label": "τ-bench", "variants": {"tau-bench": "v1", "tau2-bench": "τ²"}},
27 + "livebench": {"label": "LiveBench", "variants": {"livebench": "global", "livebench-2": "global"}},
28 + "arc-agi": {"label": "ARC-AGI", "variants": {"arc-agi": "1", "arc-agi-2": "2", "arc-agi-3": "3"}},
29 + "humaneval": {"label": "HumanEval", "variants": {"humaneval": "original", "humaneval-plus": "Plus", "mbpp": None}},
30 + "livecodebench": {"label": "LiveCodeBench", "variants": {"livecodebench": "rolling"}},
31 + "math": {"label": "MATH", "variants": {"math": "full", "math-500": "500"}},
32 + "terminal-bench": {"label": "Terminal-Bench", "variants": {"terminal-bench": "1.0", "terminal-bench-2": "2.0"}},
33 + "ifeval": {"label": "IFEval", "variants": {"ifeval": "original", "ifbench": "IFBench"}},
34 + "artificial-analysis-intelligence-index": {"label": "Artificial Analysis Intelligence Index", "variants": {"artificial-analysis-intelligence-index": "index"}},
35 + "humanitys-last-exam": {"label": "Humanity's Last Exam", "variants": {"humanitys-last-exam": "full"}},
36 + "scicode": {"label": "SciCode", "variants": {"scicode": "main"}},
37 + "aider-polyglot": {"label": "Aider polyglot", "variants": {"aider-polyglot": "polyglot"}},
38 + "lmarena": {"label": "LMArena", "variants": {"lmarena-text": "text", "lmarena-vision": "vision", "lmarena-webdev": "webdev"}},
39 + "mteb": {"label": "MTEB", "variants": {"mteb": "v1", "mteb-v2": "v2", "mmteb": "MMTEB"}},
40 +}
41 +_KEY_TO_FAMILY: dict[str, tuple[str, str | None]] = {k: (fam, v) for fam, spec in FAMILIES.items() for k, v in spec["variants"].items()}
42 +
43 +
44 +def family_of(benchmark_key: str) -> tuple[str | None, str | None]:
45 + """(family key, variant label) for a registry key / slug."""
46 + if benchmark_key in _KEY_TO_FAMILY:
47 + return _KEY_TO_FAMILY[benchmark_key]
48 + for fam in FAMILIES:
49 + if benchmark_key.startswith(fam):
50 + return fam, benchmark_key[len(fam):].strip("-") or None
51 + return None, None
52 +
53 +
54 +# ---------------------------------------------------------------------------------------------- metrics
55 +METRICS: dict[str, dict[str, Any]] = {
56 + # canonical metric → bounds and direction. `None` bound = unbounded.
57 + "accuracy": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
58 + "pass@1": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
59 + "pass^1": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
60 + "pass^k": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
61 + "resolved": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
62 + "pass_rate_2": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
63 + "percent_cases_well_formed": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
64 + "global_average": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
65 + "average score": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
66 + "mean score": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
67 + "index": {"min": 0, "max": 100, "higher_is_better": True, "unit": ""},
68 + "elo": {"min": 0, "max": None, "higher_is_better": True, "unit": ""},
69 + "score": {"min": None, "max": None, "higher_is_better": True, "unit": ""},
70 + "win_rate": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
71 + "f1": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
72 + "ndcg": {"min": 0, "max": 100, "higher_is_better": True, "unit": "%"},
73 + "perplexity": {"min": 0, "max": None, "higher_is_better": False, "unit": ""},
74 + "latency_ms": {"min": 0, "max": None, "higher_is_better": False, "unit": "ms"},
75 + "cost_usd": {"min": 0, "max": None, "higher_is_better": False, "unit": "USD"},
76 +}
77 +_METRIC_ALIASES = {
78 + "acc": "accuracy", "exact match": "accuracy", "em": "accuracy", "pass@1": "pass@1", "pass at 1": "pass@1", "pass^1": "pass^1", "pass1": "pass^1",
79 + "pass rate (2 attempts)": "pass_rate_2", "pass rate": "pass_rate_2", "percent resolved": "resolved", "% resolved": "resolved", "resolved rate": "resolved",
80 + "elo / bradley–terry score": "elo", "elo / bradley-terry score": "elo", "arena score": "elo", "bradley-terry": "elo", "mean score": "mean score",
81 + "average score": "average score", "global average": "global_average", "prompt-level strict accuracy": "accuracy", "strict accuracy": "accuracy",
82 + "intelligence index": "index", "composite": "index", "ppl": "perplexity",
83 +}
84 +
85 +
86 +def normalize_metric(raw: str | None) -> str | None:
87 + if not raw:
88 + return None
89 + s = raw.strip().lower()
90 + if s.startswith("category:"):
91 + return raw.strip() # LiveBench per-category averages keep their label; they are a separate metric each
92 + if s in METRICS:
93 + return s
94 + return _METRIC_ALIASES.get(s) or (s if re.fullmatch(r"[a-z0-9_@^%.\- ]+", s) else None)
95 +
96 +
97 +def metric_bounds(metric: str | None, unit: str | None = None) -> tuple[float | None, float | None]:
98 + m = normalize_metric(metric)
99 + if m and m.startswith("category:"):
100 + return 0, 100
101 + spec = METRICS.get(m or "")
102 + if spec:
103 + return spec["min"], spec["max"]
104 + if unit == "%":
105 + return 0, 100
106 + return None, None
107 +
108 +
109 +# ---------------------------------------------------------------------------------------------- trust levels
110 +TRUST_LEVELS = ("official-model-card", "official-benchmark", "peer-reviewed", "independent-evaluator", "community", "unverified")
111 +TRUST_LABELS = {
112 + "official-model-card": "Official model card / technical report (self-reported)",
113 + "official-benchmark": "Official benchmark leaderboard (submissions checked by the benchmark owner)",
114 + "peer-reviewed": "Peer-reviewed paper",
115 + "independent-evaluator": "Independent third-party evaluator",
116 + "community": "Community-run leaderboard or submission",
117 + "unverified": "Unverified / unknown provenance",
118 +}
119 +# source key (domain) → trust level of results it publishes
120 +SOURCE_TRUST: dict[str, str] = {
121 + "swebench.com": "official-benchmark", "aider.chat": "official-benchmark", "livebench.ai": "official-benchmark", "artificialanalysis.ai": "independent-evaluator",
122 + "lmarena.ai": "independent-evaluator", "scale.com": "independent-evaluator", "epoch.ai": "independent-evaluator", "vals.ai": "independent-evaluator",
123 + "huggingface.co": "community", "github.com": "community", "arxiv.org": "peer-reviewed", "openreview.net": "peer-reviewed",
124 +}
125 +OFFICIAL_LAB_SOURCES = {"docs.claude.com", "platform.openai.com", "ai.google.dev", "docs.mistral.ai", "api-docs.deepseek.com", "docs.cohere.com",
126 + "docs.x.ai", "ai.meta.com", "llama.com", "qwenlm.github.io", "developer.nvidia.com", "machinelearning.apple.com"}
127 +
128 +
129 +def trust_level(source_key: str | None, config: dict[str, Any] | None = None, *, extractor: str = "deterministic") -> str:
130 + cfg = config or {}
131 + if source_key in SOURCE_TRUST:
132 + level = SOURCE_TRUST[source_key]
133 + if level == "official-benchmark" and cfg.get("checked_by_swebench") is False:
134 + return "community"
135 + if level == "official-benchmark" and str(cfg.get("submission", "")).lower() in ("self-reported", "self reported", "unverified"):
136 + return "community"
137 + return level
138 + if source_key in OFFICIAL_LAB_SOURCES or cfg.get("self_reported") or cfg.get("source_kind") == "model_card":
139 + return "official-model-card"
140 + if extractor == "llm":
141 + return "unverified"
142 + return "unverified"
143 +
144 +
145 +# ---------------------------------------------------------------------------------------------- comparability
146 +# Config keys that change the *task* (must match for full comparability).
147 +TASK_KEYS = ("variant", "board", "dataset_revision", "harness", "evaluator", "index_version", "release", "version", "subset", "split", "shots",
148 + "pass_count", "attempts", "language", "scaffold", "agent", "system")
149 +# Config keys that change the *conditions* (mismatch → partially comparable).
150 +CONDITION_KEYS = ("reasoning_effort", "reasoning", "thinking_budget", "temperature", "judge", "tools", "tool_use", "max_tokens", "context_length",
151 + "sampling", "aggregation", "edit_format", "model_tag")
152 +# Keys that are pure bookkeeping (never affect comparability).
153 +IGNORED_KEYS = {"aa_slug", "livebench_model_id", "api_model_id", "date", "submission", "checked_by_swebench", "open_source_system", "system_org",
154 + "total_cost_usd", "cost_per_instance_usd", "dirname", "command", "versions", "test_cases", "seconds_per_case", "estimated",
155 + "livebench_hf_link", "source_kind", "self_reported", "subtasks", "notes", "url"}
156 +
157 +
158 +def _clean(v: Any) -> Any:
159 + if isinstance(v, str):
160 + return v.strip().lower()
161 + return v
162 +
163 +
164 +def config_key(config: dict[str, Any] | None, metric: str | None = None) -> str:
165 + """Stable hash of the comparability-relevant part of a result configuration (task keys + metric)."""
166 + cfg = config or {}
167 + core = {k: _clean(cfg[k]) for k in TASK_KEYS if k in cfg and cfg[k] not in (None, "", [], {})}
168 + core["metric"] = normalize_metric(metric) or ""
169 + return hashlib.sha1(json.dumps(core, sort_keys=True, default=str).encode()).hexdigest()[:12]
170 +
171 +
172 +def comparability(a_cfg: dict[str, Any] | None, b_cfg: dict[str, Any] | None, a_metric: str | None = None, b_metric: str | None = None,
173 + *, same_benchmark: bool = True) -> tuple[str, list[str]]:
174 + """Return (level, reasons). `same_benchmark` False → not comparable outright."""
175 + reasons: list[str] = []
176 + if not same_benchmark:
177 + return NOT_COMPARABLE, ["different benchmark variants"]
178 + ma, mb = normalize_metric(a_metric), normalize_metric(b_metric)
179 + if ma != mb and (ma or mb):
180 + return NOT_COMPARABLE, [f"different metrics ({a_metric} vs {b_metric})"]
181 + a, b = a_cfg or {}, b_cfg or {}
182 + for k in TASK_KEYS:
183 + if k in a or k in b:
184 + va, vb = _clean(a.get(k)), _clean(b.get(k))
185 + if va != vb and va not in (None, "") and vb not in (None, ""):
186 + reasons.append(f"{k}: {a.get(k)} vs {b.get(k)}")
187 + if reasons:
188 + return NOT_COMPARABLE, reasons
189 + for k in CONDITION_KEYS:
190 + if k in a or k in b:
191 + va, vb = _clean(a.get(k)), _clean(b.get(k))
192 + if va != vb:
193 + reasons.append(f"{k}: {a.get(k) if k in a else 'unspecified'} vs {b.get(k) if k in b else 'unspecified'}")
194 + if reasons:
195 + return PARTIAL, reasons
196 + return COMPARABLE, ["same variant, metric and evaluation conditions"]
197 +
198 +
199 +def variant_from_config(config: dict[str, Any] | None) -> str | None:
200 + cfg = config or {}
201 + for k in ("variant", "board", "subset", "split"):
202 + v = cfg.get(k)
203 + if isinstance(v, str) and v.strip():
204 + return v.strip()
205 + return None
206 +
207 +
208 +def run_group_from_config(config: dict[str, Any] | None) -> str | None:
209 + """The 'run' a result belongs to (a LiveBench release, an aider run date, an AA index version…). One current row per run group."""
210 + cfg = config or {}
211 + for k in ("release", "index_version", "version", "date", "dataset_revision"):
212 + v = cfg.get(k)
213 + if v not in (None, ""):
214 + return str(v)
215 + return None
216 +
217 +
218 +__all__ = ["COMPARABLE", "CONDITION_KEYS", "FAMILIES", "IGNORED_KEYS", "METRICS", "NOT_COMPARABLE", "PARTIAL", "TASK_KEYS", "TRUST_LABELS", "TRUST_LEVELS",
219 + "comparability", "config_key", "family_of", "metric_bounds", "normalize_metric", "run_group_from_config", "trust_level", "variant_from_config"]
added src/aiatlas/ontology/licenses.py +276 −0
@@ -0,0 +1,276 @@
1 +"""Licence ontology.
2 +
3 +A licence is identified by its SPDX id when one exists (`Apache-2.0`, `MIT`, `CC-BY-NC-4.0`) and by a stable AI Atlas key otherwise
4 +(`llama-3.1-community`, `gemma-terms`, `openrail-m`). Every entry states, as booleans (or `None` when the text is ambiguous), what the
5 +licence permits — so the UI can say "commercial use allowed / derivatives allowed / redistribution allowed / acceptable-use restrictions"
6 +instead of the meaningless "open source" for a model whose weights merely download.
7 +
8 +`normalize_license(raw)` maps the strings observed in the wild (Hugging Face `license:` tags, docs pages, GitHub) to a canonical key.
9 +Unknown strings return `None` — the raw label is kept separately by the writer (`license_raw`); nothing is guessed.
10 +"""
11 +from __future__ import annotations
12 +
13 +import re
14 +from dataclasses import dataclass, field
15 +
16 +# Categories are about *what the text allows*, not about ideology.
17 +CATEGORIES = ("permissive", "copyleft", "creative-commons", "responsible-ai", "community", "research-only", "proprietary", "unknown")
18 +
19 +
20 +@dataclass(frozen=True)
21 +class LicenseInfo:
22 + key: str # canonical key (SPDX id when applicable)
23 + label: str # display name
24 + category: str # one of CATEGORIES
25 + spdx: str | None = None # SPDX identifier when the licence is in the SPDX list
26 + url: str | None = None # canonical text
27 + commercial_use: bool | None = None # may be used commercially
28 + redistribution: bool | None = None # weights/code may be redistributed
29 + derivatives: bool | None = None # fine-tunes / modifications allowed
30 + hosting_restrictions: bool | None = None # restrictions on hosting the model as a service for third parties (incl. MAU caps)
31 + attribution: bool | None = None # attribution / notice required
32 + acceptable_use: bool | None = None # an acceptable-use policy restricts what the model may do
33 + osi_approved: bool = False # OSI-approved open-source licence (code) — never true for custom model licences
34 + weights_downloadable: bool = True # False for proprietary API-only terms
35 + aliases: tuple[str, ...] = field(default_factory=tuple)
36 +
37 + def as_dict(self) -> dict[str, object]:
38 + return {
39 + "key": self.key, "label": self.label, "category": self.category, "spdx": self.spdx, "url": self.url,
40 + "commercial_use": self.commercial_use, "redistribution": self.redistribution, "derivatives": self.derivatives,
41 + "hosting_restrictions": self.hosting_restrictions, "attribution": self.attribution, "acceptable_use": self.acceptable_use,
42 + "osi_approved": self.osi_approved, "weights_downloadable": self.weights_downloadable,
43 + }
44 +
45 +
46 +def _permissive(key: str, label: str, spdx: str, url: str, **kw: object) -> LicenseInfo:
47 + base: dict[str, object] = dict(key=key, label=label, category="permissive", spdx=spdx, url=url, commercial_use=True, redistribution=True,
48 + derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False, osi_approved=True)
49 + base.update(kw)
50 + return LicenseInfo(**base) # type: ignore[arg-type]
51 +
52 +
53 +LICENSES: dict[str, LicenseInfo] = {}
54 +
55 +
56 +def _add(info: LicenseInfo) -> None:
57 + LICENSES[info.key] = info
58 +
59 +
60 +# ---------------------------------------------------------------------------------------------- permissive / copyleft (OSI)
61 +_add(_permissive("Apache-2.0", "Apache License 2.0", "Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0",
62 + aliases=("apache 2.0", "apache-2", "apache2", "apache license 2.0", "apache license, version 2.0", "apache")))
63 +_add(_permissive("MIT", "MIT License", "MIT", "https://opensource.org/license/mit", aliases=("mit license", "the mit license")))
64 +_add(LicenseInfo(key="MIT-Modified", label="Modified MIT License", category="permissive", spdx=None, commercial_use=True, redistribution=True,
65 + derivatives=True, hosting_restrictions=None, attribution=True, acceptable_use=None, aliases=("modified mit", "mit-modified")))
66 +_add(_permissive("BSD-3-Clause", "BSD 3-Clause License", "BSD-3-Clause", "https://opensource.org/license/bsd-3-clause",
67 + aliases=("bsd-3", "bsd 3-clause", "bsd3", "new bsd", "modified bsd", "bsd")))
68 +_add(_permissive("BSD-2-Clause", "BSD 2-Clause License", "BSD-2-Clause", "https://opensource.org/license/bsd-2-clause", aliases=("bsd-2", "simplified bsd")))
69 +_add(_permissive("ISC", "ISC License", "ISC", "https://opensource.org/license/isc-license-txt"))
70 +_add(_permissive("Unlicense", "The Unlicense", "Unlicense", "https://unlicense.org", attribution=False, aliases=("unlicense",)))
71 +_add(_permissive("0BSD", "Zero-Clause BSD", "0BSD", "https://opensource.org/license/0bsd", attribution=False))
72 +_add(LicenseInfo(key="GPL-3.0", label="GNU GPL v3", category="copyleft", spdx="GPL-3.0-only", url="https://www.gnu.org/licenses/gpl-3.0.html",
73 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False,
74 + osi_approved=True, aliases=("gpl-3.0-only", "gpl-3.0-or-later", "gplv3", "gpl3", "gpl-3")))
75 +_add(LicenseInfo(key="GPL-2.0", label="GNU GPL v2", category="copyleft", spdx="GPL-2.0-only", url="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html",
76 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False,
77 + osi_approved=True, aliases=("gpl-2.0-only", "gpl-2.0-or-later", "gplv2", "gpl2", "gpl-2")))
78 +_add(LicenseInfo(key="LGPL-3.0", label="GNU LGPL v3", category="copyleft", spdx="LGPL-3.0-only", url="https://www.gnu.org/licenses/lgpl-3.0.html",
79 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False,
80 + osi_approved=True, aliases=("lgpl-3.0-only", "lgpl-3.0-or-later", "lgplv3", "lgpl")))
81 +_add(LicenseInfo(key="AGPL-3.0", label="GNU AGPL v3", category="copyleft", spdx="AGPL-3.0-only", url="https://www.gnu.org/licenses/agpl-3.0.html",
82 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=True, attribution=True, acceptable_use=False,
83 + osi_approved=True, aliases=("agpl-3.0-only", "agpl-3.0-or-later", "agplv3", "agpl")))
84 +_add(LicenseInfo(key="MPL-2.0", label="Mozilla Public License 2.0", category="copyleft", spdx="MPL-2.0", url="https://www.mozilla.org/MPL/2.0/",
85 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False,
86 + osi_approved=True, aliases=("mpl", "mozilla public license 2.0")))
87 +_add(LicenseInfo(key="EPL-2.0", label="Eclipse Public License 2.0", category="copyleft", spdx="EPL-2.0", url="https://www.eclipse.org/legal/epl-2.0/",
88 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False, osi_approved=True))
89 +
90 +# ---------------------------------------------------------------------------------------------- Creative Commons
91 +def _cc(key: str, label: str, url: str, *, nc: bool, nd: bool, sa: bool, aliases: tuple[str, ...]) -> LicenseInfo:
92 + return LicenseInfo(key=key, label=label, category="creative-commons", spdx=key, url=url, commercial_use=not nc, redistribution=True,
93 + derivatives=not nd, hosting_restrictions=nc or None, attribution=True, acceptable_use=False, osi_approved=False, aliases=aliases)
94 +
95 +
96 +_add(_cc("CC-BY-4.0", "Creative Commons Attribution 4.0", "https://creativecommons.org/licenses/by/4.0/", nc=False, nd=False, sa=False,
97 + aliases=("cc by 4.0", "cc-by", "cc by", "cc-by-4", "creative commons attribution 4.0")))
98 +_add(_cc("CC-BY-SA-4.0", "Creative Commons Attribution-ShareAlike 4.0", "https://creativecommons.org/licenses/by-sa/4.0/", nc=False, nd=False, sa=True,
99 + aliases=("cc by-sa 4.0", "cc-by-sa", "cc by-sa")))
100 +_add(_cc("CC-BY-NC-4.0", "Creative Commons Attribution-NonCommercial 4.0", "https://creativecommons.org/licenses/by-nc/4.0/", nc=True, nd=False, sa=False,
101 + aliases=("cc by-nc 4.0", "cc-by-nc", "cc by-nc", "cc-by-nc-4", "creative commons attribution non commercial 4.0")))
102 +_add(_cc("CC-BY-NC-SA-4.0", "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", "https://creativecommons.org/licenses/by-nc-sa/4.0/", nc=True, nd=False, sa=True,
103 + aliases=("cc by-nc-sa 4.0", "cc-by-nc-sa", "cc by-nc-sa")))
104 +_add(_cc("CC-BY-NC-ND-4.0", "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0", "https://creativecommons.org/licenses/by-nc-nd/4.0/", nc=True, nd=True, sa=False,
105 + aliases=("cc by-nc-nd 4.0", "cc-by-nc-nd")))
106 +_add(_cc("CC-BY-ND-4.0", "Creative Commons Attribution-NoDerivatives 4.0", "https://creativecommons.org/licenses/by-nd/4.0/", nc=False, nd=True, sa=False,
107 + aliases=("cc by-nd 4.0", "cc-by-nd")))
108 +_add(LicenseInfo(key="CC0-1.0", label="Creative Commons Zero 1.0 (public domain dedication)", category="creative-commons", spdx="CC0-1.0",
109 + url="https://creativecommons.org/publicdomain/zero/1.0/", commercial_use=True, redistribution=True, derivatives=True,
110 + hosting_restrictions=False, attribution=False, acceptable_use=False, aliases=("cc0", "cc0 1.0", "public domain")))
111 +_add(LicenseInfo(key="ODC-By-1.0", label="Open Data Commons Attribution", category="creative-commons", spdx="ODC-By-1.0", url="https://opendatacommons.org/licenses/by/1-0/",
112 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=True, acceptable_use=False, aliases=("odc-by",)))
113 +_add(LicenseInfo(key="PDDL-1.0", label="Open Data Commons Public Domain Dedication", category="creative-commons", spdx="PDDL-1.0", url="https://opendatacommons.org/licenses/pddl/",
114 + commercial_use=True, redistribution=True, derivatives=True, hosting_restrictions=False, attribution=False, acceptable_use=False, aliases=("pddl",)))
115 +
116 +# ---------------------------------------------------------------------------------------------- responsible-AI (RAIL) family
117 +def _rail(key: str, label: str, url: str, aliases: tuple[str, ...]) -> LicenseInfo:
118 + return LicenseInfo(key=key, label=label, category="responsible-ai", spdx=None, url=url, commercial_use=True, redistribution=True, derivatives=True,
119 + hosting_restrictions=False, attribution=True, acceptable_use=True, osi_approved=False, aliases=aliases)
120 +
121 +
122 +_add(_rail("OpenRAIL-M", "Open RAIL-M", "https://www.licenses.ai/ai-licenses", ("openrail", "openrail-m", "open rail-m", "creativeml-openrail-m", "creativeml openrail-m")))
123 +_add(_rail("OpenRAIL++-M", "Open RAIL++-M (Stable Diffusion XL)", "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENSE.md", ("openrail++", "openrail++-m")))
124 +_add(_rail("BigScience-BLOOM-RAIL-1.0", "BigScience BLOOM RAIL 1.0", "https://huggingface.co/spaces/bigscience/license", ("bigscience-bloom-rail-1.0", "bloom-rail", "bigscience-openrail-m")))
125 +_add(_rail("BigCode-OpenRAIL-M", "BigCode Open RAIL-M", "https://www.bigcode-project.org/docs/pages/bigcode-openrail/", ("bigcode-openrail-m",)))
126 +
127 +# ---------------------------------------------------------------------------------------------- community / custom model licences (weights download, restrictions apply)
128 +def _community(key: str, label: str, url: str, aliases: tuple[str, ...], *, commercial: bool | None = True, hosting: bool | None = True,
129 + derivatives: bool | None = True, aup: bool | None = True) -> LicenseInfo:
130 + return LicenseInfo(key=key, label=label, category="community", spdx=None, url=url, commercial_use=commercial, redistribution=True, derivatives=derivatives,
131 + hosting_restrictions=hosting, attribution=True, acceptable_use=aup, osi_approved=False, aliases=aliases)
132 +
133 +
134 +_add(_community("Llama-2-Community", "Llama 2 Community License", "https://ai.meta.com/llama/license/", ("llama2", "llama-2", "llama 2 community license", "llama2 community license")))
135 +_add(_community("Llama-3-Community", "Llama 3 Community License", "https://llama.meta.com/llama3/license/", ("llama3", "llama-3", "llama 3 community license", "meta llama 3 community license")))
136 +_add(_community("Llama-3.1-Community", "Llama 3.1 Community License", "https://llama.meta.com/llama3_1/license/", ("llama3.1", "llama-3.1", "llama 3.1 community license")))
137 +_add(_community("Llama-3.2-Community", "Llama 3.2 Community License", "https://www.llama.com/llama3_2/license/", ("llama3.2", "llama-3.2", "llama 3.2 community license")))
138 +_add(_community("Llama-3.3-Community", "Llama 3.3 Community License", "https://www.llama.com/llama3_3/license/", ("llama3.3", "llama-3.3", "llama 3.3 community license")))
139 +_add(_community("Llama-4-Community", "Llama 4 Community License", "https://www.llama.com/llama4/license/", ("llama4", "llama-4", "llama 4 community license")))
140 +_add(_community("Gemma-Terms", "Gemma Terms of Use", "https://ai.google.dev/gemma/terms", ("gemma", "gemma terms of use", "gemma license", "gemma-terms")))
141 +_add(_community("Mistral-Research", "Mistral AI Research License", "https://mistral.ai/licenses/MRL-0.1.md", ("mrl", "mistral research license", "mistral ai research license", "mrl-0.1"),
142 + commercial=False, hosting=True, aup=True))
143 +_add(_community("Mistral-Non-Production", "Mistral AI Non-Production License", "https://mistral.ai/licenses/MNPL-0.1.md", ("mnpl", "mnpl-0.1", "mistral ai non-production license"),
144 + commercial=False, hosting=True, aup=True))
145 +_add(_community("Qwen-Research", "Qwen Research License", "https://huggingface.co/Qwen/Qwen2.5-3B/blob/main/LICENSE", ("qwen research license", "qwen-research"), commercial=False))
146 +_add(_community("Tongyi-Qianwen", "Tongyi Qianwen License", "https://github.com/QwenLM/Qwen/blob/main/Tongyi%20Qianwen%20LICENSE%20AGREEMENT", ("tongyi-qianwen", "tongyi qianwen license", "qianwen"),
147 + commercial=True, hosting=True))
148 +_add(_community("DeepSeek-Model", "DeepSeek Model License", "https://github.com/deepseek-ai/DeepSeek-LLM/blob/main/LICENSE-MODEL", ("deepseek", "deepseek license", "deepseek model license"), hosting=False))
149 +_add(_community("NVIDIA-Open-Model", "NVIDIA Open Model License", "https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/",
150 + ("nvidia open model license", "nvidia-open-model-license", "nvidia open model"), hosting=False))
151 +_add(_community("NVIDIA-Community-Model", "NVIDIA Community Model License", "https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-community-models-license/",
152 + ("nvidia community model license", "nvidia-community-model-license"), hosting=None))
153 +_add(_community("Apple-AMLR", "Apple Sample Code / ML Research License", "https://github.com/apple/ml-fastvlm/blob/main/LICENSE_MODEL", ("apple-amlr", "apple-ascl", "apple ml research license"),
154 + commercial=False, hosting=True, derivatives=True))
155 +_add(_community("Cohere-CC-BY-NC", "Cohere Labs Acceptable Use (CC-BY-NC)", "https://cohere.com/c4ai-cc-by-nc-license", ("cohere", "c4ai", "cc-by-nc-4.0 + acceptable use"), commercial=False))
156 +_add(_community("Stability-Community", "Stability AI Community License", "https://stability.ai/community-license-agreement", ("stabilityai-ai-community", "stability ai community license", "stabilityai-community"),
157 + commercial=True, hosting=True))
158 +_add(_community("Stability-Non-Commercial-Research", "Stability AI Non-Commercial Research Community License", "https://stability.ai/license",
159 + ("stabilityai-nc-research-community", "stability ai non-commercial research community"), commercial=False))
160 +_add(_community("Falcon-LLM", "Falcon LLM License (TII)", "https://falconllm.tii.ae/falcon-terms-and-conditions.html", ("falcon-llm-license", "tii falcon license", "falcon"), hosting=None))
161 +_add(_community("Yi", "Yi Series Models Community License", "https://github.com/01-ai/Yi/blob/main/MODEL_LICENSE_AGREEMENT.txt", ("yi-license", "yi license", "yi"), commercial=True))
162 +_add(_community("ChatGLM", "ChatGLM / GLM Model License", "https://github.com/THUDM/ChatGLM3/blob/main/MODEL_LICENSE", ("glm-4", "chatglm", "glm license", "zhipu"), commercial=True))
163 +_add(_community("Phi", "Microsoft Phi (MIT with terms)", "https://huggingface.co/microsoft/phi-4/blob/main/LICENSE", ("phi",), commercial=True, hosting=False))
164 +_add(_community("LFM-Open-1.0", "LFM Open License v1.0 (Liquid AI)", "https://www.liquid.ai/lfm-license", ("lfm1.0", "lfm-1.0", "lfm open license"), commercial=True, hosting=True))
165 +_add(_community("Jamba-Open-Model", "Jamba Open Model License (AI21)", "https://www.ai21.com/jamba-open-model-license/", ("jamba open model license", "jamba"), commercial=True))
166 +_add(_community("Kimi-Modified-MIT", "Kimi Modified MIT License (Moonshot)", "https://huggingface.co/moonshotai/Kimi-K2-Instruct/blob/main/LICENSE", ("kimi modified mit", "moonshot modified mit"),
167 + commercial=True, hosting=True, aup=False))
168 +_add(_community("SEED-Model", "ByteDance Seed Model License", "https://github.com/ByteDance-Seed", ("seed license", "bytedance seed"), commercial=None))
169 +_add(_community("Hunyuan-Community", "Tencent Hunyuan Community License", "https://github.com/Tencent/Tencent-Hunyuan-Large/blob/main/License.docx", ("tencent-hunyuan-community", "hunyuan", "tencent hunyuan community license"),
170 + commercial=True, hosting=True))
171 +_add(_community("IBM-Granite", "IBM Granite (Apache-2.0 with usage terms)", "https://www.ibm.com/granite", ("granite",), commercial=True, hosting=False, aup=True))
172 +_add(_community("Custom-Community", "Custom community licence (see source)", "", ("custom community", "community license", "other-community"), commercial=None, hosting=None, derivatives=None, aup=None))
173 +
174 +# ---------------------------------------------------------------------------------------------- research-only / non-commercial custom
175 +_add(LicenseInfo(key="Research-Only", label="Research / non-commercial licence (custom)", category="research-only", spdx=None, commercial_use=False, redistribution=None,
176 + derivatives=None, hosting_restrictions=True, attribution=True, acceptable_use=True, aliases=("research", "research only", "research-only", "non-commercial", "noncommercial",
177 + "academic", "academic use only", "non commercial")))
178 +_add(LicenseInfo(key="Meta-Research", label="Meta Research License", category="research-only", spdx=None, url="https://ai.meta.com/resources/models-and-libraries/",
179 + commercial_use=False, redistribution=False, derivatives=True, hosting_restrictions=True, attribution=True, acceptable_use=True,
180 + aliases=("meta research license", "fair noncommercial research license", "fair-noncommercial")))
181 +
182 +# ---------------------------------------------------------------------------------------------- proprietary
183 +_add(LicenseInfo(key="Proprietary", label="Proprietary (API / terms of service)", category="proprietary", spdx=None, commercial_use=None, redistribution=False,
184 + derivatives=False, hosting_restrictions=True, attribution=None, acceptable_use=True, weights_downloadable=False,
185 + aliases=("proprietary", "closed", "closed source", "commercial", "terms of service", "tos", "api terms", "license: proprietary")))
186 +_add(LicenseInfo(key="Other", label="Other (unclassified licence)", category="unknown", spdx=None, aliases=("other", "unknown", "custom", "see model card", "license: other")))
187 +
188 +
189 +# ---------------------------------------------------------------------------------------------- normalisation
190 +_ALIAS_INDEX: dict[str, str] = {}
191 +for _info in LICENSES.values():
192 + _ALIAS_INDEX[_info.key.lower()] = _info.key
193 + if _info.spdx:
194 + _ALIAS_INDEX[_info.spdx.lower()] = _info.key
195 + for _a in _info.aliases:
196 + _ALIAS_INDEX[_a.lower()] = _info.key
197 +
198 +_STRIP = re.compile(r"\s+|[_]")
199 +_PATTERNS: list[tuple[re.Pattern[str], str]] = [
200 + (re.compile(r"^apache[\s\-]*(license)?[\s\-,]*(version)?[\s\-]*2(\.0)?$", re.I), "Apache-2.0"),
201 + (re.compile(r"^llama[\s\-]*4", re.I), "Llama-4-Community"),
202 + (re.compile(r"^llama[\s\-]*3[.\-]3", re.I), "Llama-3.3-Community"),
203 + (re.compile(r"^llama[\s\-]*3[.\-]2", re.I), "Llama-3.2-Community"),
204 + (re.compile(r"^llama[\s\-]*3[.\-]1", re.I), "Llama-3.1-Community"),
205 + (re.compile(r"^(meta[\s\-]*)?llama[\s\-]*3", re.I), "Llama-3-Community"),
206 + (re.compile(r"^(meta[\s\-]*)?llama[\s\-]*2", re.I), "Llama-2-Community"),
207 + (re.compile(r"^gemma", re.I), "Gemma-Terms"),
208 + (re.compile(r"^cc[\s\-]*by[\s\-]*nc[\s\-]*sa", re.I), "CC-BY-NC-SA-4.0"),
209 + (re.compile(r"^cc[\s\-]*by[\s\-]*nc[\s\-]*nd", re.I), "CC-BY-NC-ND-4.0"),
210 + (re.compile(r"^cc[\s\-]*by[\s\-]*nc", re.I), "CC-BY-NC-4.0"),
211 + (re.compile(r"^cc[\s\-]*by[\s\-]*sa", re.I), "CC-BY-SA-4.0"),
212 + (re.compile(r"^cc[\s\-]*by[\s\-]*nd", re.I), "CC-BY-ND-4.0"),
213 + (re.compile(r"^cc[\s\-]*by", re.I), "CC-BY-4.0"),
214 + (re.compile(r"^cc0", re.I), "CC0-1.0"),
215 + (re.compile(r"^(creativeml[\s\-]*)?openrail\+\+", re.I), "OpenRAIL++-M"),
216 + (re.compile(r"^(creativeml[\s\-]*)?openrail", re.I), "OpenRAIL-M"),
217 + (re.compile(r"^bigscience", re.I), "BigScience-BLOOM-RAIL-1.0"),
218 + (re.compile(r"^bigcode", re.I), "BigCode-OpenRAIL-M"),
219 + (re.compile(r"^(gpl|gnu general public license)[\s\-]*v?3", re.I), "GPL-3.0"),
220 + (re.compile(r"^(gpl|gnu general public license)[\s\-]*v?2", re.I), "GPL-2.0"),
221 + (re.compile(r"^agpl", re.I), "AGPL-3.0"),
222 + (re.compile(r"^lgpl", re.I), "LGPL-3.0"),
223 + (re.compile(r"^bsd[\s\-]*3", re.I), "BSD-3-Clause"),
224 + (re.compile(r"^bsd[\s\-]*2", re.I), "BSD-2-Clause"),
225 + (re.compile(r"^mit\b", re.I), "MIT"),
226 + (re.compile(r"^mistral.*research", re.I), "Mistral-Research"),
227 + (re.compile(r"^mistral.*non[\s\-]*production", re.I), "Mistral-Non-Production"),
228 + (re.compile(r"^nvidia.*open.*model", re.I), "NVIDIA-Open-Model"),
229 + (re.compile(r"^nvidia.*community", re.I), "NVIDIA-Community-Model"),
230 + (re.compile(r"^deepseek", re.I), "DeepSeek-Model"),
231 + (re.compile(r"^qwen.*research", re.I), "Qwen-Research"),
232 + (re.compile(r"^tongyi", re.I), "Tongyi-Qianwen"),
233 + (re.compile(r"^stability.*(nc|non[\s\-]*commercial)", re.I), "Stability-Non-Commercial-Research"),
234 + (re.compile(r"^stability", re.I), "Stability-Community"),
235 + (re.compile(r"^apple", re.I), "Apple-AMLR"),
236 + (re.compile(r"^(tencent[\s\-]*)?hunyuan", re.I), "Hunyuan-Community"),
237 + (re.compile(r"^lfm", re.I), "LFM-Open-1.0"),
238 + (re.compile(r"^jamba", re.I), "Jamba-Open-Model"),
239 + (re.compile(r"^falcon", re.I), "Falcon-LLM"),
240 + (re.compile(r"^yi\b", re.I), "Yi"),
241 + (re.compile(r"(research|academic|non[\s\-]*commercial|noncommercial)", re.I), "Research-Only"),
242 + (re.compile(r"(proprietary|closed|terms of service)", re.I), "Proprietary"),
243 +]
244 +
245 +
246 +def normalize_license(raw: str | None) -> str | None:
247 + """Map an observed licence label to a canonical key, or `None` when it cannot be classified (keep the raw string separately)."""
248 + if raw is None:
249 + return None
250 + s = str(raw).strip()
251 + if not s:
252 + return None
253 + low = s.lower().strip().strip(".")
254 + low = re.sub(r"^license:\s*", "", low)
255 + if low in _ALIAS_INDEX:
256 + return _ALIAS_INDEX[low]
257 + compact = _STRIP.sub("-", low).replace("--", "-")
258 + if compact in _ALIAS_INDEX:
259 + return _ALIAS_INDEX[compact]
260 + for pattern, key in _PATTERNS:
261 + if pattern.search(low):
262 + return key
263 + return None
264 +
265 +
266 +def license_info(key_or_raw: str | None) -> LicenseInfo | None:
267 + key = key_or_raw if key_or_raw in LICENSES else normalize_license(key_or_raw)
268 + return LICENSES.get(key) if key else None
269 +
270 +
271 +def license_label(key_or_raw: str | None) -> str | None:
272 + info = license_info(key_or_raw)
273 + return info.label if info else (str(key_or_raw) if key_or_raw else None)
274 +
275 +
276 +__all__ = ["CATEGORIES", "LICENSES", "LicenseInfo", "license_info", "license_label", "normalize_license"]
added src/aiatlas/ontology/models.py +271 −0
@@ -0,0 +1,271 @@
1 +"""Model identity ontology — the rules that separate a MODEL (release) from its ARTIFACTS (checkpoints, conversions, quantisations),
2 +its EVALUATION VARIANTS (reasoning-effort settings of the same weights) and its FAMILY.
3 +
4 + MODEL FAMILY Llama 4 · Qwen3.6 · Claude · Gemini
5 + └ MODEL Llama 4 Maverick · Qwen3.6-35B-A3B · Claude Fable 5.1
6 + └ ARTIFACT meta-llama/Llama-4-Maverick-17B-128E-Instruct (official checkpoint) · unsloth/…-GGUF (third-party quantisation)
7 + └ DEPLOYMENT OpenRouter meta-llama/llama-4-maverick · Together meta-llama/Llama-4-Maverick (= prices rows)
8 +
9 +Everything here is deterministic string analysis. It never *asserts* a relation on its own: the resolution service uses these
10 +signals together with identifiers, organisations, `base_model` metadata and dates, and parks anything ambiguous in the review queue.
11 +"""
12 +from __future__ import annotations
13 +
14 +import re
15 +from dataclasses import dataclass, field
16 +
17 +# ---------------------------------------------------------------------------------------------- quantisation / precision formats
18 +QUANT_FORMATS: dict[str, str] = {
19 + # token (lowercase, matched on word boundaries) → canonical format
20 + "gguf": "gguf", "ggml": "gguf", "q2_k": "gguf", "q3_k_m": "gguf", "q4_k_m": "gguf", "q4_k_s": "gguf", "q5_k_m": "gguf", "q6_k": "gguf", "q8_0": "gguf",
21 + "q4_0": "gguf", "q4_1": "gguf", "q5_0": "gguf", "iq4_xs": "gguf", "iq3_m": "gguf", "iq2_m": "gguf", "ud-q4_k_xl": "gguf",
22 + "awq": "awq", "w4a16": "awq", "gptq": "gptq", "exl2": "exl2", "exl3": "exl3", "mlx": "mlx", "onnx": "onnx", "openvino": "openvino", "coreml": "coreml",
23 + "tensorrt": "tensorrt", "trt": "tensorrt", "trt-llm": "tensorrt", "bnb": "bnb", "bitsandbytes": "bnb", "nf4": "bnb", "4bit": "int4", "8bit": "int8",
24 + "int4": "int4", "int8": "int8", "w8a8": "int8", "w8a16": "int8", "fp8": "fp8", "fp8-kv": "fp8", "e4m3": "fp8", "nvfp4": "nvfp4", "mxfp4": "mxfp4",
25 + "mxfp8": "mxfp8", "fp4": "fp4", "quark": "quark", "quantized": "quantized", "quant": "quantized", "qat": "qat", "hqq": "hqq", "aqlm": "aqlm", "eetq": "eetq",
26 + "smoothquant": "smoothquant", "compressed-tensors": "compressed-tensors", "marlin": "gptq", "gptq-int4": "gptq", "autoround": "autoround",
27 + "2bit": "int2", "3bit": "int3", "5bit": "int5", "6bit": "int6", "2-bit": "int2", "3-bit": "int3", "4-bit": "int4", "5-bit": "int5", "6-bit": "int6", "8-bit": "int8",
28 +}
29 +# Full-precision dtype tokens: a "-BF16" or "-FP16" repo is a *conversion/packaging* of the same weights, not a quantisation.
30 +PRECISION_FORMATS = {"bf16", "fp16", "fp32", "f16", "f32", "float16", "bfloat16", "half"}
31 +ARTIFACT_PACKAGING = {"safetensors", "pytorch", "pth", "ckpt", "jax", "flax", "tf", "tflite", "litert", "gguf", "mlx", "onnx", "coreml", "openvino", "tensorrt"}
32 +
33 +# Third-party organisations that (almost) only publish conversions/quantisations of other people's models.
34 +CONVERTER_ORGS = {
35 + "unsloth", "bartowski", "mlx-community", "thebloke", "lmstudio-community", "qwen-community", "mradermacher", "quantfactory", "nousresearch-quant",
36 + "mistral-community", "turboderp", "casperhansen", "neuralmagic", "redhatai", "amd", "intel", "nvidia-quant", "ggml-org", "second-state",
37 + "mlx-vision", "cortexso", "gaianet", "bunnycore", "dranger003", "ubergarm", "anthracite-org", "modelcloud", "jinaai-quant", "ai-forever-quant",
38 + "lmstudio", "ollama", "ggerganov", "mlc-ai", "onnx-community", "onnxmodelzoo", "kaitchup", "thedrummer-quant",
39 +}
40 +
41 +# ---------------------------------------------------------------------------------------------- evaluation-effort variants (same weights, different setting)
42 +# Suffixes appended by evaluators (Artificial Analysis, LiveBench…) to a model slug to denote a *configuration* of the model.
43 +EFFORT_SUFFIXES: dict[str, dict[str, str]] = {
44 + "xhigh": {"reasoning_effort": "xhigh"}, "x-high": {"reasoning_effort": "xhigh"}, "extra-high": {"reasoning_effort": "xhigh"},
45 + "high": {"reasoning_effort": "high"}, "medium": {"reasoning_effort": "medium"}, "low": {"reasoning_effort": "low"}, "minimal": {"reasoning_effort": "minimal"},
46 + "max": {"reasoning_effort": "max"}, "max-effort": {"reasoning_effort": "max"}, "high-effort": {"reasoning_effort": "high"}, "low-effort": {"reasoning_effort": "low"},
47 + "medium-effort": {"reasoning_effort": "medium"}, "xhigh-effort": {"reasoning_effort": "xhigh"},
48 + "thinking": {"reasoning": "on"}, "reasoning": {"reasoning": "on"}, "think": {"reasoning": "on"}, "thinking-on": {"reasoning": "on"},
49 + "non-reasoning": {"reasoning": "off"}, "no-reasoning": {"reasoning": "off"}, "non-thinking": {"reasoning": "off"}, "nothink": {"reasoning": "off"},
50 + "no-think": {"reasoning": "off"}, "instant": {"reasoning": "off"}, "thinking-off": {"reasoning": "off"}, "fast": {"reasoning": "off"},
51 + "adaptive": {"reasoning": "adaptive"}, "adaptive-reasoning": {"reasoning": "adaptive"},
52 + "thinking-16k": {"reasoning": "on", "thinking_budget": "16k"}, "thinking-32k": {"reasoning": "on", "thinking_budget": "32k"},
53 + "thinking-64k": {"reasoning": "on", "thinking_budget": "64k"}, "thinking-128k": {"reasoning": "on", "thinking_budget": "128k"},
54 + "thinking-8k": {"reasoning": "on", "thinking_budget": "8k"}, "thinking-4k": {"reasoning": "on", "thinking_budget": "4k"}, "thinking-1k": {"reasoning": "on", "thinking_budget": "1k"},
55 +}
56 +# order matters: try the longest compound suffixes first
57 +_EFFORT_ORDERED = sorted(EFFORT_SUFFIXES, key=len, reverse=True)
58 +_EFFORT_RE = re.compile(r"[-_ ](" + "|".join(re.escape(s) for s in _EFFORT_ORDERED) + r")$", re.I)
59 +# Words that are *part of a model name*, never an effort suffix, when they precede the suffix (e.g. "Kimi K2 Thinking" is a distinct release).
60 +OFFICIAL_THINKING_RELEASES = {"kimi-k2-thinking", "qwen3-235b-a22b-thinking-2507", "qwen3-30b-a3b-thinking-2507", "qwen3-4b-thinking-2507", "glm-4.5-air-thinking",
61 + "gemini-2.5-flash-thinking", "grok-3-mini-thinking", "gemini-2-0-flash-thinking-exp-1219", "gemini-2-0-flash-thinking-exp-01-21"}
62 +
63 +# ---------------------------------------------------------------------------------------------- name analysis
64 +_SIZE_RE = re.compile(r"(?<![a-z0-9])(\d+(?:\.\d+)?)\s?([bmt])(?![a-z])", re.I) # 70B 3.8B 235B 1.5T 350M
65 +_ACTIVE_RE = re.compile(r"(?<![a-z0-9])(\d+(?:\.\d+)?)\s?[bmt]\s?-?a(\d+(?:\.\d+)?)\s?([bmt])(?![a-z])", re.I) # 35B-A3B
66 +_DATE_RE = re.compile(r"(?<!\d)(20\d{2})[-_.]?(0[1-9]|1[0-2])[-_.]?(0[1-9]|[12]\d|3[01])(?!\d)") # 20240620
67 +_MMDD_RE = re.compile(r"(?<![0-9])(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])(?![0-9])") # 0905 / 0324 (release snapshots)
68 +_YYMM_RE = re.compile(r"(?<![0-9])(2[3-9])(0[1-9]|1[0-2])(?![0-9])") # 2507 (Qwen-style)
69 +_TRAILING_VARIANT_WORDS = {"instruct", "it", "chat", "base", "sft", "dpo", "rl", "hf", "preview", "exp", "experimental", "latest", "beta", "alpha", "v1", "v2", "v3",
70 + "mtp", "distill", "distilled", "uncensored", "abliterated", "heretic", "merge"}
71 +_TOKEN_SPLIT = re.compile(r"[\s_/]+|(?<=[a-z])(?=[A-Z][a-z])")
72 +
73 +
74 +@dataclass
75 +class NameAnalysis:
76 + raw: str
77 + repo_org: str | None = None # "unsloth" in "unsloth/Qwen3.6-35B-A3B-GGUF"
78 + repo_name: str | None = None # "Qwen3.6-35B-A3B-GGUF"
79 + base_key: str = "" # normalised key with quant/precision/effort tokens removed: "qwen3.6-35b-a3b"
80 + quant_formats: list[str] = field(default_factory=list) # ["gguf"]
81 + precision: str | None = None # "bf16" when only a precision tag is present
82 + is_quantized: bool = False
83 + is_conversion: bool = False # packaging/precision conversion without quantisation (BF16 repack, MLX fp16, ONNX)
84 + from_converter_org: bool = False
85 + effort: dict[str, str] = field(default_factory=dict) # {"reasoning_effort": "xhigh"} when an evaluator suffix was stripped
86 + effort_suffix: str | None = None
87 + parameter_count: int | None = None
88 + active_parameter_count: int | None = None
89 + snapshot_date: str | None = None # "2024-06-20" / "2025-09" style date embedded in the name
90 + family_hint: str | None = None # "Qwen3.6", "Llama 4", "Claude", "Gemini 2.5"
91 +
92 + @property
93 + def is_artifact(self) -> bool:
94 + return self.is_quantized or self.is_conversion
95 +
96 + @property
97 + def is_effort_variant(self) -> bool:
98 + return bool(self.effort)
99 +
100 +
101 +def _to_count(num: str, unit: str) -> int:
102 + mult = {"m": 1e6, "b": 1e9, "t": 1e12}[unit.lower()]
103 + return int(round(float(num) * mult))
104 +
105 +
106 +def analyze_model_name(raw: str) -> NameAnalysis:
107 + """Deterministic analysis of a model / repository name."""
108 + a = NameAnalysis(raw=raw.strip())
109 + name = a.raw
110 + if "/" in name and " " not in name.split("/")[0]:
111 + org, _, rest = name.partition("/")
112 + a.repo_org, a.repo_name = org.strip(), rest.strip()
113 + name = rest
114 + a.from_converter_org = org.strip().lower() in CONVERTER_ORGS
115 + low = re.sub(r"[\s_]+", "-", name.lower().strip())
116 + low = re.sub(r"[()\[\]]+", "-", low).strip("-")
117 + low = re.sub(r"-{2,}", "-", low)
118 +
119 + # evaluator effort suffix (only when the remaining stem is not itself an official "Thinking" release)
120 + m = _EFFORT_RE.search(low)
121 + if m and low not in OFFICIAL_THINKING_RELEASES:
122 + suffix = m.group(1).lower()
123 + a.effort = dict(EFFORT_SUFFIXES[suffix])
124 + a.effort_suffix = suffix
125 + low = low[: m.start()]
126 +
127 + # sizes
128 + am = _ACTIVE_RE.search(low)
129 + if am:
130 + a.parameter_count = _to_count(am.group(1), low[am.start(1) + len(am.group(1)):].strip()[0])
131 + a.active_parameter_count = _to_count(am.group(2), am.group(3))
132 + else:
133 + sizes = _SIZE_RE.findall(low)
134 + if sizes:
135 + counts = [_to_count(n, u) for n, u in sizes]
136 + a.parameter_count = max(counts)
137 +
138 + # dates embedded in the name
139 + dm = _DATE_RE.search(low)
140 + if dm:
141 + a.snapshot_date = f"{dm.group(1)}-{dm.group(2)}-{dm.group(3)}"
142 + else:
143 + ym = _YYMM_RE.search(low)
144 + if ym and not _SIZE_RE.search(ym.group(0)):
145 + a.snapshot_date = f"20{ym.group(1)}-{ym.group(2)}"
146 +
147 + # quantisation / precision tokens
148 + tokens = [t for t in re.split(r"[-_\s./()\[\]]+", low) if t]
149 + quant: list[str] = []
150 + precision = None
151 + kept: list[str] = []
152 + for t in tokens:
153 + if t in QUANT_FORMATS:
154 + quant.append(QUANT_FORMATS[t])
155 + continue
156 + if t in PRECISION_FORMATS:
157 + precision = t
158 + continue
159 + if re.fullmatch(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)", t):
160 + quant.append(QUANT_FORMATS.get(t, "quantized"))
161 + continue
162 + kept.append(t)
163 + # "GGUF" / "MLX" packaging counts as artifact even without a bit-width; "MLX-8bit" is quantised
164 + a.quant_formats = sorted(set(quant))
165 + a.precision = precision
166 + a.is_quantized = any(q not in ("onnx", "coreml", "openvino", "tensorrt", "mlx", "gguf") for q in a.quant_formats) or "gguf" in a.quant_formats
167 + a.is_conversion = (not a.is_quantized) and (bool(a.quant_formats) or precision is not None or (a.from_converter_org and bool(a.repo_org)))
168 + if "mlx" in a.quant_formats and any(q.startswith("int") for q in a.quant_formats):
169 + a.is_quantized = True
170 +
171 + a.base_key = "-".join(kept).strip("-")
172 + a.family_hint = family_hint(name)
173 + return a
174 +
175 +
176 +# ---------------------------------------------------------------------------------------------- family inference
177 +_FAMILY_PATTERNS: list[tuple[re.Pattern[str], str]] = [
178 + (re.compile(r"\bclaude\b", re.I), "Claude"),
179 + (re.compile(r"\bgpt[- ]?(oss)\b", re.I), "gpt-oss"),
180 + (re.compile(r"\b(chat)?gpt[- ]?\d", re.I), "GPT"),
181 + (re.compile(r"\bo[1-9](-| |$|mini|pro)", re.I), "OpenAI o-series"),
182 + (re.compile(r"\bgemini\b", re.I), "Gemini"),
183 + (re.compile(r"\bgemma(?=\d|\b)", re.I), "Gemma"),
184 + (re.compile(r"\bpalm\b", re.I), "PaLM"),
185 + (re.compile(r"\bllama(?=\d|\b)", re.I), "Llama"),
186 + (re.compile(r"\bmistral\b", re.I), "Mistral"),
187 + (re.compile(r"\bmixtral\b", re.I), "Mixtral"),
188 + (re.compile(r"\bministral\b", re.I), "Ministral"),
189 + (re.compile(r"\b(codestral|devstral|magistral|pixtral|voxtral)\b", re.I), None), # own families, name = family
190 + (re.compile(r"\bqwen(?=\d|\b)|\bqwq\b|\bqvq\b", re.I), "Qwen"),
191 + (re.compile(r"\bdeepseek\b", re.I), "DeepSeek"),
192 + (re.compile(r"\bkimi\b", re.I), "Kimi"),
193 + (re.compile(r"\bglm(?=\d|\b)|\bchatglm\b", re.I), "GLM"),
194 + (re.compile(r"\bgrok\b", re.I), "Grok"),
195 + (re.compile(r"\bcommand\b", re.I), "Command"),
196 + (re.compile(r"\baya\b", re.I), "Aya"),
197 + (re.compile(r"\bphi(?=\d|\b)", re.I), "Phi"),
198 + (re.compile(r"\bnemotron\b", re.I), "Nemotron"),
199 + (re.compile(r"\bgranite\b", re.I), "Granite"),
200 + (re.compile(r"\bolmo(?=\d|\b)", re.I), "OLMo"),
201 + (re.compile(r"\bmolmo\b", re.I), "Molmo"),
202 + (re.compile(r"\bfalcon\b", re.I), "Falcon"),
203 + (re.compile(r"\byi\b", re.I), "Yi"),
204 + (re.compile(r"\bminimax\b", re.I), "MiniMax"),
205 + (re.compile(r"\bhunyuan\b", re.I), "Hunyuan"),
206 + (re.compile(r"\bernie\b", re.I), "ERNIE"),
207 + (re.compile(r"\bseed\b", re.I), "Seed"),
208 + (re.compile(r"\bdoubao\b", re.I), "Doubao"),
209 + (re.compile(r"\bstep\b", re.I), "Step"),
210 + (re.compile(r"\binternlm(?=\d|\b)|\binternvl(?=\d|\b)", re.I), "InternLM"),
211 + (re.compile(r"\bjamba\b", re.I), "Jamba"),
212 + (re.compile(r"\blfm(?=\d|\b)", re.I), "LFM"),
213 + (re.compile(r"\bexaone(?=\d|\b)", re.I), "EXAONE"),
214 + (re.compile(r"\bsolar\b", re.I), "Solar"),
215 + (re.compile(r"\bnova\b", re.I), "Nova"),
216 + (re.compile(r"\btitan\b", re.I), "Titan"),
217 + (re.compile(r"\bsonar\b", re.I), "Sonar"),
218 + (re.compile(r"\bstable[- ]?diffusion\b|\bsdxl\b|\bsd3\b", re.I), "Stable Diffusion"),
219 + (re.compile(r"\bflux\b", re.I), "FLUX"),
220 + (re.compile(r"\bwhisper\b", re.I), "Whisper"),
221 + (re.compile(r"\bdall[- ]?e\b", re.I), "DALL·E"),
222 + (re.compile(r"\bsora\b", re.I), "Sora"),
223 + (re.compile(r"\bveo\b", re.I), "Veo"),
224 + (re.compile(r"\bimagen\b", re.I), "Imagen"),
225 + (re.compile(r"\bcogito\b", re.I), "Cogito"),
226 + (re.compile(r"\bhermes\b", re.I), "Hermes"),
227 + (re.compile(r"\bsmol(lm|vlm)\b", re.I), "SmolLM"),
228 + (re.compile(r"\bbert\b", re.I), "BERT"),
229 + (re.compile(r"\bt5\b", re.I), "T5"),
230 + (re.compile(r"\bclip\b", re.I), "CLIP"),
231 + (re.compile(r"\bembed(ding)?\b", re.I), None),
232 + (re.compile(r"\brerank\b", re.I), None),
233 +]
234 +_FAMILY_VERSION_RE = re.compile(r"^(?P<fam>[A-Za-z][A-Za-z·\-]*?)[\s\-]?(?P<ver>\d+(?:\.\d+)?)", re.I)
235 +
236 +
237 +def family_hint(name: str) -> str | None:
238 + """Family label without a version ("Qwen", "Llama", "Claude"). Versioned families ("Llama 3.1") are `family_release_hint`."""
239 + n = name.split("/")[-1]
240 + for pat, fam in _FAMILY_PATTERNS:
241 + if pat.search(n):
242 + if fam is None:
243 + m = pat.search(n)
244 + return m.group(0).title() if m else None
245 + return fam
246 + return None
247 +
248 +
249 +def family_release_hint(name: str) -> str | None:
250 + """Versioned family ("Llama 3.1", "Qwen3.6", "Gemini 2.5", "Claude 4") when the name carries a version right after the family word."""
251 + fam = family_hint(name)
252 + if not fam:
253 + return None
254 + n = name.split("/")[-1]
255 + m = re.search(re.escape(fam.split(" ")[0]) + r"[\s\-]?(\d+(?:\.\d+)?)", n, re.I)
256 + if m:
257 + joined = fam in ("Qwen", "GLM", "Phi", "Yi", "Step") or re.search(re.escape(fam) + r"\d", n, re.I)
258 + return f"{fam}{m.group(1)}" if joined else f"{fam} {m.group(1)}"
259 + return fam
260 +
261 +
262 +def variant_key(name: str) -> str:
263 + """Grouping key for near-duplicates: analysed base key + parameter count, ignoring org prefixes, quant/precision/effort tokens,
264 + separators and trailing 'instruct/chat/it' words. `Qwen3.6-35B-A3B`, `unsloth/Qwen3.6-35B-A3B-GGUF`, `Qwen3.6 35B A3B FP8` → same key."""
265 + a = analyze_model_name(name)
266 + toks = [t for t in a.base_key.split("-") if t and t not in _TRAILING_VARIANT_WORDS]
267 + return "-".join(toks)
268 +
269 +
270 +__all__ = ["ARTIFACT_PACKAGING", "CONVERTER_ORGS", "EFFORT_SUFFIXES", "NameAnalysis", "PRECISION_FORMATS", "QUANT_FORMATS", "analyze_model_name",
271 + "family_hint", "family_release_hint", "variant_key"]
added src/aiatlas/ontology/openness.py +115 −0
@@ -0,0 +1,115 @@
1 +"""Openness 2.0 — measurable dimensions first, a carefully labelled category second.
2 +
3 +Dimensions (each `True | False | None` = unknown):
4 + weights_available the weights can be downloaded (a checkpoint exists on a public hub / official download)
5 + source_code_available inference / model code is published
6 + training_code_available the training pipeline is published
7 + training_data_disclosed the composition of the training data is documented
8 + dataset_available the training data itself (or most of it) is downloadable
9 + commercial_use_allowed licence permits commercial use
10 + redistribution_allowed licence permits redistributing the weights
11 + derivatives_allowed licence permits fine-tunes / modifications
12 +
13 +Categories (`OPENNESS_CATEGORIES`) are derived, never asserted directly by a connector unless the source literally states it:
14 + open-source weights + code under an OSI-approved (or equivalently permissive) licence, commercial use and derivatives allowed
15 + open-weights weights downloadable under a permissive or Creative Commons licence allowing commercial use (code may be missing)
16 + restricted-weights weights downloadable but the licence restricts commercial use, hosting, derivatives or field of use
17 + proprietary weights not available (API / product only)
18 + unknown not enough evidence
19 +
20 +The historical single-value vocabulary (`open-weights | open-source | proprietary | restricted`) stays valid as input to `normalize_openness`.
21 +"""
22 +from __future__ import annotations
23 +
24 +from typing import Any
25 +
26 +from aiatlas.ontology.licenses import LICENSES, normalize_license
27 +
28 +OPENNESS_CATEGORIES = ("open-source", "open-weights", "restricted-weights", "proprietary", "unknown")
29 +
30 +OPENNESS_DIMENSIONS = (
31 + "weights_available", "source_code_available", "training_code_available", "training_data_disclosed", "dataset_available",
32 + "commercial_use_allowed", "redistribution_allowed", "derivatives_allowed",
33 +)
34 +
35 +_LEGACY = {
36 + "open-weights": "open-weights", "open_weights": "open-weights", "open weights": "open-weights", "openweights": "open-weights", "open": "open-weights",
37 + "open-source": "open-source", "open_source": "open-source", "open source": "open-source", "opensource": "open-source", "oss": "open-source",
38 + "proprietary": "proprietary", "closed": "proprietary", "closed-source": "proprietary", "closed_source": "proprietary", "closed source": "proprietary",
39 + "api-only": "proprietary", "api only": "proprietary", "commercial": "proprietary",
40 + "restricted": "restricted-weights", "restricted-weights": "restricted-weights", "restricted_weights": "restricted-weights", "gated": "restricted-weights",
41 + "research-only": "restricted-weights", "non-commercial": "restricted-weights",
42 + "unknown": "unknown", "": "unknown",
43 +}
44 +
45 +
46 +def normalize_openness(raw: Any) -> str | None:
47 + """Legacy single label → canonical category. Unknown strings → None (the writer keeps the raw label)."""
48 + if raw is None:
49 + return None
50 + s = str(raw).strip().lower()
51 + return _LEGACY.get(s)
52 +
53 +
54 +def openness_dimensions(*, weights_available: bool | None = None, license_key: str | None = None, license_raw: str | None = None,
55 + source_code_available: bool | None = None, training_code_available: bool | None = None,
56 + training_data_disclosed: bool | None = None, dataset_available: bool | None = None) -> dict[str, bool | None]:
57 + """Fill the licence-derived dimensions from the licence ontology; everything else is passed through (unknown stays None)."""
58 + key = license_key or normalize_license(license_raw)
59 + info = LICENSES.get(key) if key else None
60 + dims: dict[str, bool | None] = {
61 + "weights_available": weights_available,
62 + "source_code_available": source_code_available,
63 + "training_code_available": training_code_available,
64 + "training_data_disclosed": training_data_disclosed,
65 + "dataset_available": dataset_available,
66 + "commercial_use_allowed": info.commercial_use if info else None,
67 + "redistribution_allowed": info.redistribution if info else None,
68 + "derivatives_allowed": info.derivatives if info else None,
69 + }
70 + if info and not info.weights_downloadable and weights_available is None:
71 + dims["weights_available"] = False
72 + return dims
73 +
74 +
75 +def derive_openness(dims: dict[str, bool | None], *, license_key: str | None = None) -> str:
76 + """Category from dimensions (+ licence category). Conservative: a custom community licence is never 'open-source'."""
77 + info = LICENSES.get(license_key) if license_key else None
78 + weights = dims.get("weights_available")
79 + if weights is False:
80 + return "proprietary"
81 + if weights is None:
82 + return "unknown"
83 + commercial = dims.get("commercial_use_allowed")
84 + derivatives = dims.get("derivatives_allowed")
85 + redistribution = dims.get("redistribution_allowed")
86 + restricted_by_license = bool(info and (info.category in ("community", "research-only", "responsible-ai") or info.hosting_restrictions))
87 + if commercial is False or derivatives is False or redistribution is False or restricted_by_license:
88 + return "restricted-weights"
89 + if info and info.osi_approved and dims.get("source_code_available") is True and commercial is True and derivatives is True:
90 + return "open-source"
91 + if info and info.category in ("permissive", "creative-commons") and commercial is True and redistribution is True:
92 + return "open-weights"
93 + if info is None:
94 + # weights downloadable, licence unknown → we know the weights exist, not the terms
95 + return "open-weights" if commercial is None else "restricted-weights"
96 + return "open-weights"
97 +
98 +
99 +OPENNESS_LABELS = {
100 + "open-source": "Open source",
101 + "open-weights": "Open weights",
102 + "restricted-weights": "Restricted weights",
103 + "proprietary": "Closed / proprietary",
104 + "unknown": "Unknown",
105 +}
106 +
107 +OPENNESS_DEFINITIONS = {
108 + "open-source": "Weights and code published under an OSI-approved licence that allows commercial use and derivatives.",
109 + "open-weights": "Weights downloadable under a permissive or Creative Commons licence allowing commercial use; code or data may be missing.",
110 + "restricted-weights": "Weights downloadable, but the licence restricts commercial use, hosting, derivatives or field of use (community, research and RAIL licences).",
111 + "proprietary": "Weights are not available; the model is reachable only through an API or a product.",
112 + "unknown": "Not enough sourced evidence to classify.",
113 +}
114 +
115 +__all__ = ["OPENNESS_CATEGORIES", "OPENNESS_DEFINITIONS", "OPENNESS_DIMENSIONS", "OPENNESS_LABELS", "derive_openness", "normalize_openness", "openness_dimensions"]
added src/aiatlas/ontology/taxonomy.py +166 −0
@@ -0,0 +1,166 @@
1 +"""Small canonical enums and their normalisers. Raw source strings are never mutated in snapshots — only the materialised attribute
2 +is canonical; connectors and the writer call these before writing a claim."""
3 +from __future__ import annotations
4 +
5 +import re
6 +from typing import Any
7 +
8 +# ---------------------------------------------------------------------------------------------- modalities
9 +MODALITIES = ("text", "image", "audio", "video", "document", "code", "embedding", "3d", "structured", "action")
10 +_MODALITY_ALIASES = {
11 + "text": "text", "texts": "text", "language": "text", "nlp": "text", "chat": "text", "textual": "text",
12 + "image": "image", "images": "image", "vision": "image", "visual": "image", "picture": "image", "photo": "image", "img": "image",
13 + "audio": "audio", "speech": "audio", "voice": "audio", "sound": "audio", "music": "audio",
14 + "video": "video", "videos": "video",
15 + "pdf": "document", "document": "document", "documents": "document", "file": "document", "files": "document", "doc": "document",
16 + "code": "code", "coding": "code", "programming": "code",
17 + "embedding": "embedding", "embeddings": "embedding", "vector": "embedding",
18 + "3d": "3d", "mesh": "3d", "point cloud": "3d",
19 + "structured": "structured", "json": "structured", "table": "structured", "tabular": "structured", "time-series": "structured", "time series": "structured",
20 + "action": "action", "actions": "action", "computer-use": "action", "computer use": "action", "tool": "action", "robotics": "action",
21 + "multimodal": None, "multi-modal": None, "omni": None, "any": None, # not a modality — a property of the set
22 +}
23 +
24 +
25 +def normalize_modality(raw: Any) -> str | None:
26 + if raw is None:
27 + return None
28 + s = str(raw).strip().lower().replace("_", "-")
29 + if s in _MODALITY_ALIASES:
30 + return _MODALITY_ALIASES[s]
31 + s2 = s.replace("-", " ")
32 + return _MODALITY_ALIASES.get(s2)
33 +
34 +
35 +def normalize_modalities(raw: Any) -> list[str]:
36 + """Any iterable / comma string → sorted canonical unique list (unknown values dropped)."""
37 + if raw is None:
38 + return []
39 + items = raw if isinstance(raw, (list, tuple, set)) else re.split(r"[,/;+&]|\band\b|→|->", str(raw))
40 + out: set[str] = set()
41 + for it in items:
42 + m = normalize_modality(it)
43 + if m:
44 + out.add(m)
45 + return sorted(out)
46 +
47 +
48 +# ---------------------------------------------------------------------------------------------- model / entity status
49 +MODEL_STATUSES = ("announced", "preview", "active", "limited-availability", "deprecated", "retired", "archived", "unknown")
50 +_STATUS_ALIASES = {
51 + "active": "active", "available": "active", "ga": "active", "general availability": "active", "generally available": "active", "live": "active",
52 + "released": "active", "stable": "active", "current": "active", "supported": "active", "production": "active", "online": "active",
53 + "preview": "preview", "beta": "preview", "alpha": "preview", "experimental": "preview", "public preview": "preview", "early access": "preview",
54 + "research preview": "preview", "exp": "preview",
55 + "limited-availability": "limited-availability", "limited availability": "limited-availability", "limited": "limited-availability",
56 + "waitlist": "limited-availability", "private preview": "limited-availability", "invite-only": "limited-availability", "gated": "limited-availability",
57 + "deprecated": "deprecated", "legacy": "deprecated", "sunset": "deprecated", "sunsetting": "deprecated", "end-of-life announced": "deprecated",
58 + "deprecation": "deprecated",
59 + "retired": "retired", "discontinued": "retired", "shut down": "retired", "shutdown": "retired", "removed": "retired", "end of life": "retired",
60 + "eol": "retired", "decommissioned": "retired", "offline": "retired", "delisted": "retired",
61 + "announced": "announced", "upcoming": "announced", "coming soon": "announced", "planned": "announced", "unreleased": "announced",
62 + "archived": "archived",
63 + "unknown": "unknown", "": "unknown",
64 +}
65 +
66 +
67 +def normalize_status(raw: Any) -> str | None:
68 + if raw is None:
69 + return None
70 + s = str(raw).strip().lower().replace("_", "-")
71 + if s in _STATUS_ALIASES:
72 + return _STATUS_ALIASES[s]
73 + return _STATUS_ALIASES.get(s.replace("-", " "))
74 +
75 +
76 +# ---------------------------------------------------------------------------------------------- organization kinds
77 +ORG_KINDS = ("company", "lab", "university", "nonprofit", "government", "community", "consortium", "individual")
78 +_ORG_KIND_ALIASES = {
79 + "company": "company", "corporation": "company", "startup": "company", "enterprise": "company", "vendor": "company", "business": "company",
80 + "lab": "lab", "laboratory": "lab", "research lab": "lab", "research-lab": "lab", "research institute": "lab", "institute": "lab", "research": "lab",
81 + "university": "university", "academic": "university", "school": "university", "college": "university",
82 + "nonprofit": "nonprofit", "non-profit": "nonprofit", "non profit": "nonprofit", "foundation": "nonprofit", "ngo": "nonprofit",
83 + "government": "government", "public": "government", "agency": "government", "state": "government",
84 + "community": "community", "open-source community": "community", "collective": "community", "hf-community": "community", "group": "community",
85 + "consortium": "consortium", "alliance": "consortium", "coalition": "consortium",
86 + "individual": "individual", "person": "individual", "independent": "individual", "user": "individual",
87 +}
88 +# entity_type → default org kind (the entity_type stays; org_kind is the canonical sub-kind)
89 +ORG_TYPE_DEFAULT_KIND = {"company": "company", "lab": "lab", "university": "university", "organization": None}
90 +
91 +
92 +def normalize_org_kind(raw: Any) -> str | None:
93 + if raw is None:
94 + return None
95 + s = str(raw).strip().lower().replace("_", "-")
96 + return _ORG_KIND_ALIASES.get(s) or _ORG_KIND_ALIASES.get(s.replace("-", " "))
97 +
98 +
99 +# ---------------------------------------------------------------------------------------------- hardware kinds
100 +HARDWARE_KINDS = ("gpu", "accelerator", "npu", "cpu", "soc", "system", "server", "workstation", "cloud-instance", "rack")
101 +_HW_ALIASES = {
102 + "gpu": "gpu", "graphics card": "gpu", "graphics": "gpu", "dgpu": "gpu",
103 + "accelerator": "accelerator", "tpu": "accelerator", "asic": "accelerator", "ai accelerator": "accelerator", "lpu": "accelerator", "wse": "accelerator",
104 + "npu": "npu", "neural engine": "npu",
105 + "cpu": "cpu", "processor": "cpu",
106 + "soc": "soc", "system on chip": "soc", "system-on-chip": "soc", "apple silicon": "soc", "chip": "soc",
107 + "system": "system", "computer": "system", "desktop": "system", "laptop": "system", "mini": "system", "mac": "system",
108 + "server": "server", "node": "server", "dgx": "server", "appliance": "server",
109 + "workstation": "workstation",
110 + "cloud-instance": "cloud-instance", "cloud instance": "cloud-instance", "instance": "cloud-instance", "vm": "cloud-instance",
111 + "rack": "rack", "superpod": "rack", "pod": "rack", "cluster": "rack", "nvl72": "rack",
112 +}
113 +
114 +
115 +def normalize_hardware_kind(raw: Any) -> str | None:
116 + if raw is None:
117 + return None
118 + s = str(raw).strip().lower().replace("_", "-")
119 + return _HW_ALIASES.get(s) or _HW_ALIASES.get(s.replace("-", " "))
120 +
121 +
122 +# ---------------------------------------------------------------------------------------------- framework / library / tool kinds
123 +FRAMEWORK_KINDS = ("training-framework", "inference-engine", "serving-engine", "library", "runtime", "agent-framework", "orchestration",
124 + "evaluation-harness", "sdk", "tool", "application", "agent", "mcp-server", "vector-database", "observability", "data-tooling")
125 +_FW_ALIASES = {
126 + "framework": "training-framework", "training framework": "training-framework", "training-framework": "training-framework", "deep learning framework": "training-framework",
127 + "inference engine": "inference-engine", "inference-engine": "inference-engine", "inference": "inference-engine", "engine": "inference-engine",
128 + "serving": "serving-engine", "serving engine": "serving-engine", "serving-engine": "serving-engine", "server": "serving-engine",
129 + "library": "library", "lib": "library", "package": "library",
130 + "runtime": "runtime",
131 + "agent framework": "agent-framework", "agent-framework": "agent-framework", "agents": "agent-framework",
132 + "orchestration": "orchestration", "workflow": "orchestration", "pipeline": "orchestration",
133 + "evaluation": "evaluation-harness", "eval": "evaluation-harness", "evaluation harness": "evaluation-harness", "evaluation-harness": "evaluation-harness", "benchmarking": "evaluation-harness",
134 + "sdk": "sdk", "client": "sdk", "api client": "sdk",
135 + "tool": "tool", "cli": "tool", "utility": "tool",
136 + "application": "application", "app": "application", "product": "application", "ui": "application", "desktop app": "application",
137 + "agent": "agent", "coding agent": "agent", "assistant": "agent",
138 + "mcp": "mcp-server", "mcp server": "mcp-server", "mcp-server": "mcp-server", "mcp_server": "mcp-server",
139 + "vector database": "vector-database", "vector-database": "vector-database", "vector db": "vector-database", "vector store": "vector-database",
140 + "observability": "observability", "tracing": "observability", "monitoring": "observability", "experiment tracking": "observability",
141 + "data": "data-tooling", "data tooling": "data-tooling", "data-tooling": "data-tooling", "datasets": "data-tooling", "tokenizer": "data-tooling",
142 +}
143 +
144 +
145 +def normalize_framework_kind(raw: Any) -> str | None:
146 + if raw is None:
147 + return None
148 + s = str(raw).strip().lower()
149 + return _FW_ALIASES.get(s) or _FW_ALIASES.get(s.replace("_", "-")) or _FW_ALIASES.get(s.replace("-", " "))
150 +
151 +
152 +# ---------------------------------------------------------------------------------------------- generic helper
153 +def canonical_enum(value: Any, normaliser) -> tuple[Any, str | None]:
154 + """Return (canonical_or_original, raw_if_changed). Lets the writer store `x` canonical and `x_raw` when the source label differed."""
155 + if value is None:
156 + return None, None
157 + canon = normaliser(value)
158 + if canon is None:
159 + return value, None
160 + return canon, (str(value) if str(value) != canon else None)
161 +
162 +
163 +__all__ = [
164 + "FRAMEWORK_KINDS", "HARDWARE_KINDS", "MODALITIES", "MODEL_STATUSES", "ORG_KINDS", "ORG_TYPE_DEFAULT_KIND", "canonical_enum",
165 + "normalize_framework_kind", "normalize_hardware_kind", "normalize_modalities", "normalize_modality", "normalize_org_kind", "normalize_status",
166 +]
modified src/aiatlas/sdk/facts.py +10 −1
@@ -21,6 +21,11 @@ class EntityRef:
21 21 status: str | None = None
22 22 attributes: dict[str, Any] = field(default_factory=dict) # convenience: each item becomes a Claim
23 23 first_seen_hint: datetime | None = None # e.g. release date, to backdate `first_seen_at` for historical backfill
24 + # canonical hierarchy hints (model_family → model → artifact): the writer materialises `entities.family_id` / `canonical_id`
25 + family: EntityRef | None = None # model → its model_family ("Qwen3.6", "Llama 4", "Claude")
26 + canonical: EntityRef | None = None # artifact → the model it packages/quantises; alias entity → canonical entity
27 + artifact_kind: str | None = None # artifacts only: checkpoint|quantization|conversion|packaging
28 + identity_confidence: str | None = None # high|medium|low — how sure the connector is that this is one real thing
24 29 id: str | None = field(default=None, compare=False)
25 30
26 31 def key(self) -> str:
@@ -104,6 +109,9 @@ class ResultObs:
104 109 evaluated_at: datetime | None = None
105 110 source_url: str | None = None
106 111 confidence: str | None = None
112 + trust_level: str | None = None # ontology.benchmarks.TRUST_LEVELS; derived from the source when None
113 + variant: str | None = None # benchmark variant label (GPQA Diamond, Verified…); derived from config when None
114 + run_group: str | None = None # evaluation run/release the row belongs to; derived from config when None
107 115
108 116
109 117 @dataclass
@@ -196,6 +204,7 @@ MATERIAL_PROPERTIES: dict[str, tuple[str, int]] = {
196 204 "active_parameter_count": ("PARAMETERS_CHANGED", 1),
197 205 "weights_availability": ("OPENNESS_CHANGED", 3),
198 206 "openness": ("OPENNESS_CHANGED", 3),
207 + "license_key": ("LICENSE_CHANGED", 2),
199 208 "release_date": ("RELEASE_DATE_CHANGED", 1),
200 209 "knowledge_cutoff": ("KNOWLEDGE_CUTOFF_CHANGED", 1),
201 210 "latest_version": ("VERSION_RELEASED", 2),
@@ -211,7 +220,7 @@ MATERIAL_PROPERTIES: dict[str, tuple[str, int]] = {
211 220 NOISY_PREFIXES = ("metric.", "stats.", "counts.")
212 221
213 222 EVENT_CATEGORY_BY_TYPE: dict[str, str] = {
214 − "model": "model", "company": "company", "organization": "company", "lab": "company", "university": "company",
223 + "model": "model", "model_family": "model", "artifact": "model", "company": "company", "organization": "company", "lab": "company", "university": "company",
215 224 "paper": "paper", "dataset": "dataset", "benchmark": "benchmark", "provider": "provider", "framework": "framework",
216 225 "library": "framework", "repository": "repository", "tool": "tool", "agent": "tool", "hardware": "hardware",
217 226 "runtime": "framework", "quantization": "model", "regulation": "regulation", "incident": "incident", "release": "release",
218 227