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%
30.3 KB · 618 lines python
Raw Blame History
1"""Shared API helpers: errors, pagination, date parsing, Redis response cache, admin auth, IP rate limits, serialisers.23Every route returns plain dicts; `AtlasJSONResponse` renders them with orjson (datetimes → ISO-8601 UTC, Decimal → float)."""4from __future__ import annotations56import hmac7import inspect8import logging9import os10import time11from collections import defaultdict, deque12from collections.abc import Awaitable, Callable13from datetime import UTC, date, datetime14from datetime import time as dtime15from decimal import Decimal16from functools import wraps17from typing import Any1819import orjson20from fastapi import Depends, HTTPException, Query, Request21from fastapi.responses import JSONResponse22from sqlalchemy.ext.asyncio import AsyncConnection2324from aiatlas.config import settings25from aiatlas.db import execute, fetch_one, jsonb, transaction26from aiatlas.services import cache2728log = logging.getLogger("aiatlas.api")2930# ------------------------------------------------------------------------------------------------------------------ errors & JSON313233class ApiError(HTTPException):34    """Always `{"detail": "..."}` — never a stack trace."""3536    def __init__(self, status: int, detail: str):37        super().__init__(status_code=status, detail=detail)383940def _json_default(obj: Any) -> Any:41    if isinstance(obj, Decimal):42        return float(obj)43    if isinstance(obj, (set, frozenset)):44        return sorted(obj, key=str)45    if isinstance(obj, bytes):46        return obj.decode("utf-8", "replace")47    return str(obj)484950_ORJSON_OPTS = orjson.OPT_NON_STR_KEYS | orjson.OPT_UTC_Z | orjson.OPT_NAIVE_UTC515253def dumps(value: Any) -> bytes:54    return orjson.dumps(value, default=_json_default, option=_ORJSON_OPTS)555657def normalize(value: Any) -> Any:58    """Round-trip through orjson so cached and fresh responses are byte-identical (datetimes → strings, Decimal → float)."""59    return orjson.loads(dumps(value))606162class AtlasJSONResponse(JSONResponse):63    media_type = "application/json"6465    def render(self, content: Any) -> bytes:66        return dumps(content)676869# ------------------------------------------------------------------------------------------------------------------ pagination & parsing707172class Pagination:73    def __init__(self, limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):74        self.limit = limit75        self.offset = offset767778PAGINATION = Depends()  # `p: Pagination = PAGINATION` — FastAPI resolves the class from the annotation798081def page(items: list[Any], total: int, p: Pagination) -> dict[str, Any]:82    return {"items": items, "total": int(total), "limit": p.limit, "offset": p.offset}838485def parse_date(v: str | None, name: str = "date") -> date | None:86    """YYYY-MM-DD (or ISO datetime) → `date`. asyncpg needs real date objects — never cast text in SQL."""87    if v is None or v == "":88        return None89    s = v.strip().replace("Z", "+00:00")90    try:91        if len(s) == 10:92            return date.fromisoformat(s)93        return datetime.fromisoformat(s).astimezone(UTC).date()94    except ValueError as exc:95        raise ApiError(400, f"{name} must be YYYY-MM-DD, got {v!r}") from exc969798def parse_ts(v: str | None, name: str = "timestamp") -> datetime | None:99    """ISO-8601 (date or datetime) → aware UTC `datetime`."""100    if v is None or v == "":101        return None102    s = v.strip().replace("Z", "+00:00")103    try:104        dt = datetime.fromisoformat(s)105    except ValueError as exc:106        raise ApiError(400, f"{name} must be ISO-8601, got {v!r}") from exc107    if dt.tzinfo is None:108        dt = datetime.combine(dt.date(), dt.time() or dtime.min, UTC)109    return dt.astimezone(UTC)110111112def day_bounds(d: date) -> tuple[datetime, datetime]:113    start = datetime.combine(d, dtime.min, UTC)114    return start, datetime.combine(d, dtime.max, UTC).replace(microsecond=999999)115116117def csv(v: str | None) -> list[str]:118    return [x.strip() for x in (v or "").split(",") if x.strip()]119120121def num_expr(path: str) -> str:122    """Safe numeric cast of a JSON text attribute (`e.attributes->>'x'`)."""123    return f"(case when {path} ~ '^-?[0-9]+(\\.[0-9]+)?$' then ({path})::double precision end)"124125126def attr_num(key: str, alias: str = "e") -> str:127    return num_expr(f"{alias}.attributes->>'{key}'")128129130def flip_order(order_sql: str, order: str) -> str:131    """Apply `order=asc|desc` to the primary sort key of a canned ORDER BY fragment (keeps `nulls last`)."""132    if order not in ("asc", "desc"):133        return order_sql134    head, sep, tail = order_sql.partition(",")135    if order == "asc" and " desc" in head:136        head = head.replace(" desc", " asc", 1)137    elif order == "desc" and " asc" in head:138        head = head.replace(" asc", " desc", 1)139    return head + sep + tail140141142# ------------------------------------------------------------------------------------------------------------------ response cache143144145def cache_key(request: Request) -> str:146    items = sorted(request.query_params.multi_items())147    qs = "&".join(f"{k}={v}" for k, v in items)148    return f"{request.url.path}?{qs}" if qs else request.url.path149150151def cached(ttl_s: int) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:152    """Cache a GET route body in Redis (`aia:api:<path>?<sorted query>`). The route must accept `request: Request`."""153154    def deco(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:155        @wraps(fn)156        async def wrapper(*args: Any, **kwargs: Any) -> Any:157            request = kwargs.get("request") or next((a for a in args if isinstance(a, Request)), None)158            key = cache_key(request) if request is not None else None159            if key is not None:160                hit = await cache.cache_get(key)161                if hit is not None:162                    return hit163            value = normalize(await fn(*args, **kwargs))164            if key is not None:165                await cache.cache_set(key, value, ttl_s)166            return value167168        wrapper.__signature__ = inspect.signature(fn)  # type: ignore[attr-defined]169        return wrapper170171    return deco172173174# ------------------------------------------------------------------------------------------------------------------ rate limiting175176_LIMITS: dict[str, tuple[int, float]] = {"search": (60, 60.0), "views": (1, 1.0), "admin": (240, 60.0), "admin-auth-failed": (10, 60.0), "default": (600, 60.0)}177_hits: dict[tuple[str, str], deque[float]] = defaultdict(deque)178179180def trust_proxy() -> bool:181    """`x-forwarded-for` is only honoured behind a trusted reverse proxy (`settings.trust_proxy` or env `AIA_TRUST_PROXY=1`)."""182    v = getattr(settings, "trust_proxy", None)183    if v is None:184        v = os.environ.get("AIA_TRUST_PROXY", "")185    return str(v).strip().lower() in ("1", "true", "yes", "on")186187188def client_ip(request: Request) -> str:189    if trust_proxy():190        fwd = request.headers.get("x-forwarded-for")191        if fwd:192            return fwd.split(",")[0].strip()193    return request.client.host if request.client else "unknown"194195196def _bucket_hit(ip: str, bucket: str, *, record: bool = True) -> bool:197    """Sliding-window counter. Returns True when the call is allowed (and records it), False when the bucket is full."""198    n, window = _LIMITS.get(bucket, _LIMITS["default"])199    now = time.monotonic()200    q = _hits[(ip, bucket)]201    while q and q[0] <= now - window:202        q.popleft()203    if len(q) >= n:204        return False205    if record:206        q.append(now)207    if len(_hits) > 20000:208        for k in [k for k, v in _hits.items() if not v or v[-1] < now - 120]:209            _hits.pop(k, None)210    return True211212213def rate_limit(bucket: str) -> Callable[[Request], None]:214    def dep(request: Request) -> None:215        n, window = _LIMITS.get(bucket, _LIMITS["default"])216        if not _bucket_hit(client_ip(request), bucket):217            raise ApiError(429, f"rate limit exceeded for {bucket}: {n} per {int(window)} s")218219    return dep220221222# ------------------------------------------------------------------------------------------------------------------ admin auth223224225def require_admin(request: Request) -> None:226    """Token check. Failed attempts are counted per IP (10/min → 429) so the token cannot be brute-forced; must run AFTER `rate_limit('admin')`."""227    ip = client_ip(request)228    if not _bucket_hit(ip, "admin-auth-failed", record=False):229        raise ApiError(429, "too many failed admin authentications; retry in a minute")230    expected = settings.admin_token231    if not expected:232        raise ApiError(503, "admin API disabled: AIA_ADMIN_TOKEN is not configured")233    token = request.headers.get("x-aia-admin-token") or ""234    if not hmac.compare_digest(token, expected):235        _bucket_hit(ip, "admin-auth-failed")236        raise ApiError(401, "invalid or missing x-aia-admin-token")237238239ADMIN_DEPENDENCIES = [Depends(rate_limit("admin")), Depends(require_admin)]  # order matters: limit first, then auth240241242async def audit(action: str, target: str | None = None, payload: dict[str, Any] | None = None, ip: str | None = None, *, actor: str = "admin") -> None:243    """One row in `admin_audit_log` per admin action (best effort — never breaks the request)."""244    try:245        async with transaction() as conn:246            await execute(conn, "insert into admin_audit_log (actor, action, target, payload, ip) values (:a, :ac, :t, cast(:p as jsonb), :ip)",247                          a=actor, ac=action, t=target, p=jsonb(payload or {}), ip=ip)248    except Exception:  # noqa: BLE001249        log.warning("audit log write failed", extra={"action": action, "target": target})250251252# ------------------------------------------------------------------------------------------------------------------ entity serialisers253254ENTITY_FIELDS = ("id", "entity_type", "canonical_name", "slug", "description", "status", "organization_id", "attributes", "quality", "counts",255                 "first_seen_at", "last_seen_at", "updated_at", "organization_name", "organization_slug")256257258def entity_cols(alias: str = "e", prefix: str = "") -> str:259    """Select-list for an entity alias (+ its organization alias `<alias>o`) with an optional column prefix."""260    org = f"{alias}o"261    cols = [f"{alias}.{c} as {prefix}{c}" for c in ENTITY_FIELDS[:13]]262    cols += [f"{org}.canonical_name as {prefix}organization_name", f"{org}.slug as {prefix}organization_slug"]263    return ", ".join(cols)264265266def entity_join(alias: str, on: str) -> str:267    return f"join entities {alias} on {alias}.id = {on} left join entities {alias}o on {alias}o.id = {alias}.organization_id"268269270ENTITY_COLS = entity_cols("e")271ENTITY_FROM = "entities e left join entities eo on eo.id = e.organization_id"272273SUMMARY_ATTRS: dict[str, tuple[str, ...]] = {274    "model": ("family", "openness", "license", "license_key", "parameter_count", "active_parameter_count", "context_length", "max_output_tokens", "modalities",275              "release_date", "status", "knowledge_cutoff", "hf_repo", "api_model_id", "reasoning", "tool_calling"),276    "artifact": ("openness", "license", "license_key", "parameter_count", "quant_format", "file_size_gb", "weights_dtype", "hf_repo", "release_date", "base_model"),277    "company": ("country", "founded", "website", "org_kind"),278    "organization": ("country", "founded", "website", "org_kind"),279    "lab": ("country", "founded", "website", "org_kind"),280    "university": ("country", "founded", "website", "org_kind"),281    "provider": ("website", "pricing_url"),282    "benchmark": ("category", "metric", "unit"),283    "hardware": ("kind", "memory_gb", "memory_bandwidth_gbs", "release_date", "manufacturer"),284    "framework": ("kind", "latest_version", "latest_release_at", "license", "license_key", "language", "metric.stars", "repository_url", "homepage"),285    "library": ("kind", "latest_version", "latest_release_at", "license", "license_key", "language", "metric.stars", "repository_url", "homepage"),286    "runtime": ("kind", "latest_version", "latest_release_at", "license", "license_key", "language", "metric.stars", "repository_url"),287    "repository": ("latest_version", "latest_release_at", "license", "language", "metric.stars"),288    "paper": ("authors", "published_at", "arxiv_id", "primary_category"),289    "dataset": ("license", "license_key", "modality", "modalities", "publisher", "size", "access", "hf_repo", "hf_dataset", "task", "languages", "release_date"),290}291292293def _fill_license_key(etype: str, attrs: dict[str, Any], out: dict[str, Any]) -> None:294    """Canonical `license_key` derived on read when the writer has not stored it yet (raw label → ontology key; unknown labels stay absent)."""295    if "license_key" not in out and attrs.get("license"):296        from aiatlas.ontology.licenses import normalize_license297298        key = normalize_license(str(attrs["license"]))299        if key:300            out["license_key"] = key301COMPANY_TYPES = ("company", "organization", "lab", "university")302TYPE_LABELS = {"model": "Models", "company": "Companies", "organization": "Organizations", "lab": "Labs", "university": "Universities",303               "provider": "Providers", "paper": "Papers", "benchmark": "Benchmarks", "hardware": "Hardware", "framework": "Frameworks",304               "dataset": "Datasets", "tool": "Tools", "repository": "Repositories", "regulation": "Regulation", "incident": "Incidents",305               "researcher": "Researchers", "agent": "Agents", "product": "Products", "runtime": "Runtimes", "conference": "Conferences",306               "artifact": "Artifacts", "model_family": "Model families", "license": "Licenses"}307308# Canonical model universe (API 1.1): model releases only — artifacts (checkpoints, quantisations, conversions) are `entity_type = 'artifact'`,309# folded evaluation variants carry `merged_into`. `include=artifacts` on /models restores the pre-1.1 universe.310MODEL_UNIVERSE = "e.entity_type = 'model' and e.merged_into is null"311MODEL_OR_ARTIFACT_UNIVERSE = "e.entity_type in ('model', 'artifact') and e.merged_into is null"312OPEN_CATEGORIES = ("open-weights", "open-source")                                  # "open-*"313DOWNLOADABLE_CATEGORIES = ("open-weights", "open-source", "restricted-weights", "restricted")  # weights can be downloaded (terms may restrict)314ARTIFACT_KINDS = ("checkpoint", "quantization", "conversion", "packaging")315316317def openness_values(raw: str | None) -> list[str]:318    """Expand the `openness=` filter vocabulary: `open` → open-weights + open-source (+ legacy `open`); `restricted` ↔ `restricted-weights`."""319    vals = [v.strip() for v in (raw or "").split(",") if v.strip()]320    out: list[str] = []321    for v in vals:322        if v == "open":323            out += [*OPEN_CATEGORIES, "open"]324        elif v in ("restricted", "restricted-weights"):325            out += ["restricted", "restricted-weights"]326        elif v == "downloadable":327            out += list(DOWNLOADABLE_CATEGORIES)328        else:329            out.append(v)330    return list(dict.fromkeys(out))331332333def is_open(openness: Any) -> bool:334    return str(openness or "") in OPEN_CATEGORIES335336337def summary_attributes(entity_type: str, attrs: dict[str, Any] | None) -> dict[str, Any]:338    attrs = attrs or {}339    keys = SUMMARY_ATTRS.get(entity_type)340    if keys is None:341        return {k: v for k, v in attrs.items() if isinstance(v, (int, float, bool)) or (isinstance(v, str) and len(v) <= 200)}342    out: dict[str, Any] = {}343    for k in keys:344        if k in attrs and attrs[k] not in (None, "", [], {}):345            v = attrs[k]346            out[k] = v[:5] if k == "authors" and isinstance(v, list) else v347    if "license_key" in keys:348        _fill_license_key(entity_type, attrs, out)349    if "kind" in keys and out.get("kind") and entity_type in ("framework", "library", "runtime"):350        from aiatlas.ontology.taxonomy import normalize_framework_kind351352        canon = normalize_framework_kind(out["kind"])353        if canon and canon != out["kind"]:354            out["kind_raw"], out["kind"] = out["kind"], canon355    return out356357358STATUS_VOCAB = ("active", "preview", "deprecated", "retired", "announced", "limited-availability", "unknown")359STATUS_MAP = {"available": "active", "ga": "active", "general-availability": "active", "released": "active", "live": "active", "beta": "preview",360              "alpha": "preview", "experimental": "preview", "coming-soon": "announced", "upcoming": "announced", "sunset": "retired", "discontinued": "retired",361              "archived": "retired", "legacy": "deprecated", "limited": "limited-availability", "merged": "retired"}362363364def normalize_status(value: Any) -> str:365    s = str(value or "").strip().lower().replace("_", "-").replace(" ", "-")366    s = STATUS_MAP.get(s, s)367    return s if s in STATUS_VOCAB else "unknown"368369370MAIN_ENTITY_TYPES = ("model", "company", "paper", "provider", "benchmark", "hardware", "framework", "dataset", "tool", "repository")371372EVENT_TYPE_LABELS: dict[str, tuple[str, int]] = {  # event_type -> (human label, default importance 0–3)373    "NEW_MODEL": ("New model", 3), "MODEL_UPDATED": ("Model updated", 1), "PRICE_CHANGED": ("Price change", 2), "CONTEXT_CHANGED": ("Context window change", 2),374    "MAX_OUTPUT_CHANGED": ("Max output change", 1), "PARAMETERS_CHANGED": ("Parameter count change", 2), "KNOWLEDGE_CUTOFF_CHANGED": ("Knowledge cutoff change", 1),375    "RELEASE_DATE_CHANGED": ("Release date change", 1), "STATUS_CHANGED": ("Status change", 2), "LICENSE_CHANGED": ("License change", 2),376    "OPENNESS_CHANGED": ("Openness change", 2), "CAPABILITIES_CHANGED": ("Capabilities change", 1), "PROPERTY_CHANGED": ("Property change", 1),377    "DEPRECATION_ANNOUNCED": ("Deprecation announced", 2), "RETIREMENT_ANNOUNCED": ("Retirement announced", 2), "BENCHMARK_RESULT": ("Benchmark result", 1),378    "BENCHMARK_UPDATED": ("Benchmark updated", 1), "NEW_PAPER": ("New paper", 2), "NEW_COMPANY": ("New company", 2), "NEW_ORGANIZATION": ("New organization", 2),379    "NEW_LAB": ("New lab", 2), "NEW_UNIVERSITY": ("New university", 1), "NEW_PROVIDER": ("New provider", 2), "NEW_HARDWARE": ("New hardware", 2),380    "NEW_FRAMEWORK": ("New framework", 2), "NEW_LIBRARY": ("New library", 1), "NEW_BENCHMARK": ("New benchmark", 2), "NEW_DATASET": ("New dataset", 2),381    "NEW_REPOSITORY": ("New repository", 1), "NEW_TOOL": ("New tool", 1), "PROVIDER_LISTED": ("Listed by provider", 2), "PROVIDER_DELISTED": ("Delisted by provider", 2),382    "ANNOUNCEMENT": ("Announcement", 2), "RELEASE": ("Release", 2), "VERSION_RELEASED": ("Version released", 1), "DOCUMENT_CHANGED": ("Source document changed", 0),383    "ENTITY_MERGED": ("Duplicate merged", 1), "CLAIM_RETRACTED": ("Claim retracted", 1),384}385386387def event_type_label(event_type: str) -> str:388    known = EVENT_TYPE_LABELS.get(event_type)389    return known[0] if known else event_type.replace("_", " ").capitalize()390391392def event_type_importance(event_type: str) -> int:393    known = EVENT_TYPE_LABELS.get(event_type)394    return known[1] if known else 1395396397async def enrich_provenance(conn: AsyncConnection, *provenances: dict[str, Any] | None) -> None:398    """Add `source_name` (from `sources.name`) to every provenance entry that carries a `source_id` — in place."""399    ids = {v.get("source_id") for p in provenances if p for v in p.values() if isinstance(v, dict) and v.get("source_id")}400    if not ids:401        return402    from aiatlas.db import fetch_all403404    rows = await fetch_all(conn, "select id, name, domain, tier from sources where id = any(cast(:ids as text[]))", ids=sorted(ids))405    names = {r["id"]: r for r in rows}406    for p in provenances:407        for v in (p or {}).values():408            if isinstance(v, dict) and v.get("source_id") in names:409                src = names[v["source_id"]]410                v.setdefault("source_name", src["name"])411                v.setdefault("source_domain", src["domain"])412413414def org_of(row: dict[str, Any], prefix: str = "") -> dict[str, Any] | None:415    oid = row.get(f"{prefix}organization_id")416    if not oid:417        return None418    return {"id": oid, "slug": row.get(f"{prefix}organization_slug"), "name": row.get(f"{prefix}organization_name")}419420421def entity_summary(row: dict[str, Any], prefix: str = "") -> dict[str, Any] | None:422    g = row.get423    if not g(f"{prefix}id"):424        return None425    etype = g(f"{prefix}entity_type") or ""426    desc = g(f"{prefix}description")427    raw_status = g(f"{prefix}status")428    status = normalize_status(raw_status)429    return {"id": g(f"{prefix}id"), "entity_type": etype, "slug": g(f"{prefix}slug"), "name": g(f"{prefix}canonical_name"),430            "description": (desc[:280] if isinstance(desc, str) and len(desc) > 280 else desc), "status": status,431            **({"status_raw": raw_status} if raw_status and raw_status != status else {}),432            "organization": org_of(row, prefix), "attributes": summary_attributes(etype, g(f"{prefix}attributes")),433            "quality": g(f"{prefix}quality") or {}, "counts": g(f"{prefix}counts") or {},434            "first_seen_at": g(f"{prefix}first_seen_at"), "last_seen_at": g(f"{prefix}last_seen_at"), "updated_at": g(f"{prefix}updated_at")}435436437def change_event(row: dict[str, Any], prefix: str = "e_") -> dict[str, Any]:438    return {"id": row["id"], "event_type": row["event_type"], "category": row["category"], "property": row.get("property"),439            "old_value": row.get("old_value"), "new_value": row.get("new_value"), "summary": row.get("summary"), "importance": row.get("importance"),440            "observed_at": row.get("observed_at"), "effective_at": row.get("effective_at"), "source_url": row.get("source_url"),441            "connector_name": row.get("connector_name"), "entity": entity_summary(row, prefix), "meta": row.get("meta") or {}}442443444EVENT_COLS = ("ev.id, ev.event_type, ev.category, ev.property, ev.old_value, ev.new_value, ev.summary, ev.importance, ev.observed_at, ev.effective_at, "445              "ev.source_url, ev.connector_name, ev.meta, " + entity_cols("e", "e_"))446EVENT_FROM = "change_events ev left join entities e on e.id = ev.entity_id left join entities eo on eo.id = e.organization_id"447448449def price_row(row: dict[str, Any]) -> dict[str, Any]:450    return {"id": row["id"], "model": entity_summary(row, "m_"), "provider": entity_summary(row, "p_"), "provider_model_id": row.get("provider_model_id"),451            "input_per_mtok": row.get("input_per_mtok"), "output_per_mtok": row.get("output_per_mtok"), "cached_input_per_mtok": row.get("cached_input_per_mtok"),452            "cache_write_per_mtok": row.get("cache_write_per_mtok"), "batch_input_per_mtok": row.get("batch_input_per_mtok"),453            "batch_output_per_mtok": row.get("batch_output_per_mtok"), "per_image": row.get("per_image"), "per_request": row.get("per_request"),454            "currency": row.get("currency"), "context_length": row.get("context_length"), "max_output_tokens": row.get("max_output_tokens"),455            "features": row.get("features") or {}, "observed_at": row.get("observed_at"), "valid_from": row.get("valid_from"), "valid_to": row.get("valid_to"),456            "source_url": row.get("source_url"), "tier": row.get("tier")}457458459PRICE_COLS = ("p.id, p.provider_model_id, p.input_per_mtok, p.output_per_mtok, p.cached_input_per_mtok, p.cache_write_per_mtok, p.batch_input_per_mtok, "460              "p.batch_output_per_mtok, p.per_image, p.per_request, p.currency, p.context_length, p.max_output_tokens, p.features, p.observed_at, p.valid_from, "461              "p.valid_to, p.source_url, p.tier, " + entity_cols("m", "m_") + ", " + entity_cols("pv", "p_"))462PRICE_FROM = "prices p " + entity_join("m", "p.model_id") + " " + entity_join("pv", "p.provider_id")463464_NATIVE_UNIT_SUFFIXES = ("_per_mtok", "_per_1k_requests", "_per_mtok_hour", "_per_image", "_per_request", "_per_second", "_per_minute", "_per_hour", "_per_char")465466467def deployment_row(row: dict[str, Any]) -> dict[str, Any]:468    """`Deployment` (API 1.1): one model × provider × provider_model_id offer. Prices in USD per 1M tokens unless the key says otherwise;469    provider-specific priced features (`flex_input_per_mtok`, `search_grounding_per_1k_requests`…) are kept verbatim under `native_units`."""470    feats = dict(row.get("features") or {})471    native = {k: v for k, v in feats.items() if any(k.endswith(s) for s in _NATIVE_UNIT_SUFFIXES) or k == "per_request"}472    other = {k: v for k, v in feats.items() if k not in native}473    return {"id": row["id"], "model": entity_summary(row, "m_"), "provider": entity_summary(row, "p_"), "provider_model_id": row.get("provider_model_id"),474            "context_length": row.get("context_length"), "max_output_tokens": row.get("max_output_tokens"),475            "prices": {"input": row.get("input_per_mtok"), "cached_input": row.get("cached_input_per_mtok"), "cache_write": row.get("cache_write_per_mtok"),476                       "output": row.get("output_per_mtok"), "batch_input": row.get("batch_input_per_mtok"), "batch_output": row.get("batch_output_per_mtok"),477                       "per_image": row.get("per_image"), "per_request": row.get("per_request"), "currency": row.get("currency") or "USD", "unit": "USD per 1M tokens",478                       "native_units": native},479            "features": other, "status": "active" if row.get("valid_to") is None else "delisted", "observed_at": row.get("observed_at"),480            "valid_from": row.get("valid_from"), "valid_to": row.get("valid_to"), "source_url": row.get("source_url"), "tier": row.get("tier")}481482483def result_row(row: dict[str, Any]) -> dict[str, Any]:484    return {"id": row["id"], "model": entity_summary(row, "m_"), "benchmark": entity_summary(row, "b_"), "score": row.get("score"), "metric": row.get("metric"),485            "unit": row.get("unit"), "higher_is_better": row.get("higher_is_better"), "config": row.get("config") or {}, "evaluated_at": row.get("evaluated_at"),486            "observed_at": row.get("observed_at"), "source_url": row.get("source_url"), "tier": row.get("tier"), "confidence": row.get("confidence"),487            "valid_to": row.get("valid_to")}488489490RESULT_COLS = ("r.id, r.score, r.metric, r.unit, r.higher_is_better, r.config, r.evaluated_at, r.observed_at, r.source_url, r.tier, r.confidence, r.valid_to, "491               + entity_cols("m", "m_") + ", " + entity_cols("b", "b_"))492RESULT_FROM = "benchmark_results r " + entity_join("m", "r.model_id") + " " + entity_join("b", "r.benchmark_id")493RESULT_ORDER = "case when r.higher_is_better then -r.score else r.score end, r.observed_at desc"494495496def claim_row(row: dict[str, Any]) -> dict[str, Any]:497    return {"id": row["id"], "property": row["property"], "value": row.get("value"), "unit": row.get("unit"), "tier": row.get("tier"),498            "confidence": row.get("confidence"), "status": row.get("status"), "extractor": row.get("extractor"), "observed_at": row.get("observed_at"),499            "effective_at": row.get("effective_at"), "valid_from": row.get("valid_from"), "valid_to": row.get("valid_to"), "source_url": row.get("source_url"),500            "source_name": row.get("source_name")}501502503CLAIM_COLS = ("c.id, c.property, c.value, c.unit, c.tier, c.confidence, c.status, c.extractor, c.observed_at, c.effective_at, c.valid_from, c.valid_to, "504              "c.source_url, s.name as source_name")505CLAIM_FROM = "claims c left join sources s on s.id = c.source_id"506507508# ------------------------------------------------------------------------------------------------------------------ entity resolution509510511ENTITY_EXTRA_COLS = "e.provenance, e.merged_into, e.family_id, e.canonical_id, e.artifact_kind, e.identity_confidence"512513514async def resolve_entity(conn: AsyncConnection, slug_or_id: str, types: tuple[str, ...] | None = None, *, aliases: bool = False) -> dict[str, Any]:515    """Slug first, then id (then alias when `aliases=True`); follows `merged_into` and records the hop in `redirected_from`516    (`{slug, id}` of the row the caller asked for) so the web layer can 301; 404 when absent or when the type does not match the mounted alias."""517    key = slug_or_id.strip()518    if not key or len(key) > 200:519        raise ApiError(404, "entity not found")520    sel = f"select {ENTITY_COLS}, {ENTITY_EXTRA_COLS} from {ENTITY_FROM}"521    row = await fetch_one(conn, f"{sel} where e.slug = :k or e.id = :k order by case when e.slug = :k then 0 else 1 end limit 1", k=key)522    if not row and aliases:523        row = await fetch_one(conn, f"{sel} where e.id = (select a.entity_id from entity_aliases a join entities x on x.id = a.entity_id "524                                    f"where (a.alias_norm = :n or lower(a.alias) = lower(:k)) {'and x.entity_type = any(cast(:types as text[]))' if types else ''} "525                                    f"order by x.merged_into is not null, x.updated_at desc limit 1)", k=key, n=_alias_norm(key), types=list(types or ()))526    asked = row527    hops = 0528    while row and row.get("merged_into") and hops < 5:529        row = await fetch_one(conn, f"{sel} where e.id = :k", k=row["merged_into"])530        hops += 1531    if not row:532        raise ApiError(404, "entity not found")533    if types and row["entity_type"] not in types:534        raise ApiError(404, f"entity {key!r} is a {row['entity_type']}, not one of {', '.join(types)}")535    if asked is not None and asked["id"] != row["id"]:536        row["redirected_from"] = {"slug": asked["slug"], "id": asked["id"], "entity_type": asked["entity_type"]}537    return row538539540def _alias_norm(s: str) -> str:541    try:542        from aiatlas.ids import normalize_alias543544        return normalize_alias(s)545    except Exception:  # noqa: BLE001546        return "".join(ch for ch in s.lower() if ch.isalnum())547548549async def resolve_id(conn: AsyncConnection, slug_or_id: str | None, types: tuple[str, ...] | None = None) -> str | None:550    if not slug_or_id:551        return None552    return (await resolve_entity(conn, slug_or_id, types))["id"]553554555__all__ = [556    "ADMIN_DEPENDENCIES",557    "ARTIFACT_KINDS",558    "CLAIM_COLS",559    "CLAIM_FROM",560    "COMPANY_TYPES",561    "DOWNLOADABLE_CATEGORIES",562    "ENTITY_COLS",563    "ENTITY_EXTRA_COLS",564    "ENTITY_FROM",565    "EVENT_COLS",566    "EVENT_FROM",567    "EVENT_TYPE_LABELS",568    "MAIN_ENTITY_TYPES",569    "MODEL_OR_ARTIFACT_UNIVERSE",570    "MODEL_UNIVERSE",571    "OPEN_CATEGORIES",572    "PAGINATION",573    "PRICE_COLS",574    "PRICE_FROM",575    "RESULT_COLS",576    "RESULT_FROM",577    "RESULT_ORDER",578    "STATUS_VOCAB",579    "TYPE_LABELS",580    "ApiError",581    "AtlasJSONResponse",582    "Pagination",583    "attr_num",584    "audit",585    "cache_key",586    "cached",587    "change_event",588    "claim_row",589    "client_ip",590    "csv",591    "day_bounds",592    "deployment_row",593    "dumps",594    "enrich_provenance",595    "entity_cols",596    "entity_join",597    "entity_summary",598    "event_type_importance",599    "event_type_label",600    "flip_order",601    "is_open",602    "normalize",603    "normalize_status",604    "num_expr",605    "openness_values",606    "org_of",607    "page",608    "parse_date",609    "parse_ts",610    "price_row",611    "rate_limit",612    "require_admin",613    "resolve_entity",614    "resolve_id",615    "result_row",616    "trust_proxy",617]618