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%
39.7 KB

# AI Atlas API — contract (API 1.1, mounted at /api/v1)

Base: /api/v1 (FastAPI, src/aiatlas/api). JSON, UTF-8, ISO-8601 UTC timestamps, numbers as numbers (Postgres aggregates may arrive as strings — the web layer normalises). Public routes are cached in Redis (aia:api:*, 60–600 s), carry a weak ETag + Cache-Control: public, max-age=60, stale-while-revalidate=300 and answer 304 to a matching If-None-Match; search is rate-limited per IP. Every response carries x-api-version: 1.1. Admin routes require header x-aia-admin-token: <AIA_ADMIN_TOKEN> and are rate-limited before the token check (240/min per IP; 10 failed authentications per minute per IP → 429). OpenAPI at /api/v1/docs. Errors: {"detail": "..."} with 400/404/409/422/429/501/503. Never expose credentials, internal hostnames, raw archive content or raw_paths.

1.1 is additive over v1. No v1 route, field or type was removed or retyped (see "Deprecations": none). New fields and routes are listed below; three defaults changed semantics (see "Semantics changes") and each has a parameter that restores the v1 behaviour. Every number that is derived or estimated comes with a methodology, note, definition or estimated: true string next to it; when data is missing the value is null/empty with a note — nothing is fabricated.

# Changelog

version date changes
1.1 2026-09-12 Canonical model universe (/models excludes artifacts and folded variants; include=artifacts), model detail blocks (family, artifacts, deployments, identity, licence, openness, version_history, grouped benchmarks, redirected_from), backfill-aware feeds keyed on occurred_at, "Today in AI 2.0", comparability groups + one-row-per-model leaderboards, /benchmarks/matrix, /benchmarks/{slug}/frontier, grouped /compare + diff_only + /models/{a}/diff/{b}, Deployment shape + /deployments + /cost, AI Price Index, provider aggregates, /frontier, /pareto, /pulse, /open, /find-a-model, /run-locally, /hardware/{slug}/fit, /families, /graph/explore, /time-machine, /diff sections, search compiler v2 (compiled), /claims/{id}, /entities/{slug}/claims, /entities/{slug}/provenance/{property}, /licenses, richer /methodology, /trending?kind=, /stats counter definitions, admin workbenches (quality, entity-resolution, anomalies, extractions, quarantine, audit, rollback), ETag/304, admin rate limit before auth, failed-auth limiter, trust-proxy, set_limit GUC leak fixed, detail fan-out ≤ 4 connections.
1.0 2026-09-11 Initial contract (table "v1 routes" below).

# Shared shapes

ts
type Num = number | string | null
type Org = { id: string; slug: string; name: string } | null
type EntitySummary = { id: string; entity_type: string; slug: string; name: string; description: string | null; status: string;
  organization: Org; attributes: Record<string, unknown>; quality: { score?: number; completeness?: number; primary_source_ratio?: number;
  freshness?: number; source_count?: number; conflicts?: number }; counts: { relations?: number; events?: number; claims?: number };
  first_seen_at: string; last_seen_at: string; updated_at: string }
type Page<T> = { items: T[]; total: number; limit: number; offset: number }
type Provenance = Record<string, { source_id: string | null; source_name?: string; url: string | null; observed_at: string; tier: number;
  confidence: string; extractor: string; unit?: string }>
type ChangeEvent = { id: string; event_type: string; category: string; property: string | null; old_value: unknown; new_value: unknown;
  summary: string; importance: 0 | 1 | 2 | 3; observed_at: string; effective_at: string | null; source_url: string | null; connector_name: string | null;
  entity: EntitySummary | null; meta: Record<string, unknown>;
  occurred_at?: string; is_backfill?: boolean; group_key?: string | null }          // 1.1 (feeds)
type Price = { id: string; model: EntitySummary; provider: EntitySummary; provider_model_id: string | null; input_per_mtok: Num; output_per_mtok: Num;
  cached_input_per_mtok: Num; cache_write_per_mtok: Num; batch_input_per_mtok: Num; batch_output_per_mtok: Num; per_image: Num; currency: string;
  context_length: Num; max_output_tokens: Num; features: Record<string, unknown>; observed_at: string; valid_from: string; valid_to: string | null; source_url: string | null; tier: number }
type BenchmarkResult = { id: string; model: EntitySummary; benchmark: EntitySummary; score: number; metric: string | null; unit: string | null;
  higher_is_better: boolean; config: Record<string, unknown>; evaluated_at: string | null; observed_at: string; source_url: string | null; tier: number; confidence: string;
  valid_to: string | null; config_key?: string | null; trust_level?: string | null }   // 1.1 adds config_key/trust_level where returned
type Claim = { id: string; property: string; value: unknown; unit: string | null; tier: number; confidence: string; status: string; extractor: string;
  observed_at: string; effective_at: string | null; valid_from: string; valid_to: string | null; source_url: string | null; source_name: string | null }
type SourceRef = { source_id: string | null; source_name: string | null; domain: string | null; tier: number | null; url: string; doc_type: string; last_observed_at: string | null; snapshots: number }

// ---- 1.1 shapes
type Deployment = { id: string; model: EntitySummary; provider: EntitySummary; provider_model_id: string | null; context_length: Num; max_output_tokens: Num;
  prices: { input: Num; cached_input: Num; cache_write: Num; output: Num; batch_input: Num; batch_output: Num; per_image: Num; per_request: Num; currency: string;
            unit: "USD per 1M tokens"; native_units: Record<string, unknown> };   // provider-specific priced features kept verbatim (flex_*, priority_*, *_per_1k_requests…)
  features: Record<string, unknown>; status: "active" | "delisted"; observed_at: string; valid_from: string; valid_to: string | null; source_url: string | null; tier: number }
type Group = { metric: string; config_key: string; label: string; n: number; model_count: number; config: Record<string, unknown>; higher_is_better: boolean; trust_mix: Record<string, number> }
type LeaderboardRow = { rank: number; model: ModelRef; score: number; metric: string; unit: string | null; higher_is_better: boolean; delta_rank: number | null; previous_rank: number | null;
  trust_level: string; trust_label: string; config: Record<string, unknown>; config_key: string; comparability: "comparable" | "partially-comparable" | "not-comparable";
  comparability_reasons: string[]; evaluated_at: string | null; observed_at: string; source_url: string | null; tier: number; result_id: string; n_rows: number }
type ModelRef = { id: string; slug: string; name: string; entity_type: string; organization: Org; attributes: Record<string, unknown> }   // openness/params/context/release/modalities/license/family
type LicenseInfo = { key: string; label: string; category: string; spdx: string | null; url: string | null; commercial_use: boolean | null; redistribution: boolean | null;
  derivatives: boolean | null; hosting_restrictions: boolean | null; attribution: boolean | null; acceptable_use: boolean | null; osi_approved: boolean; weights_downloadable: boolean }
type Fit = { quantization: string; estimated: true; fits: boolean; estimated_memory_gb: number; headroom_gb: number; parameter_count?: number;
  breakdown: { weights_gb: number; weights_source: "observed" | "estimated"; overhead_gb: number; kv_cache_gb: number; kv_cache_method: "architecture" | "heuristic"; reserved_gb: number; context: number; batch: number };
  device: { memory_gb: number; gpu_count: number; total_memory_gb: number }; note: string; multi_gpu_note?: string }

# v1 routes (unchanged unless noted "1.1:")

route returns
GET /health `{ status: "ok"
GET /stats { entities: Record<type, number>, entities_total, sources, connectors, connectors_enabled, documents, snapshots, claims, claims_current, relations, change_events, change_events_24h, change_events_7d, benchmark_results, prices_current, prices_total, review_pending, llm_jobs, llm_tokens, last_snapshot_at, last_event_at, first_entity_at, archive: {…}, computed_at } — 1.1: entities.model = canonical models; entities.artifact, entities.model_family (zero-filled); adds organizations_total, artifacts, model_families, change_events_live_24h, change_events_live_7d, definitions: Record<key, string> ("How counted"). Cached 60 s.
GET /stats/history?days=90 { items: { day: string; counts: object }[] }
GET /search?q=&type=&limit=30&offset=0 { query: CompiledQuery, items: (EntitySummary & { rank: number })[], total: number } — 1.1: compiler v2 (below)
GET /search/suggest?q= { items: { id, entity_type, slug, name, organization_name }[] } (≤ 8, prefix, fast)
GET /entities/{slug_or_id} EntityDetail (below). Also mounted as /models/{slug} (1.1: accepts model AND artifact), /companies/{slug}, /papers/{slug}, /providers/{slug}, /benchmarks/{slug} (1.1: slug, alias or id), /hardware/{slug}, /frameworks/{slug}, /datasets/{slug}, /tools/{slug} (404 if the type doesn't match)
GET /entities/{slug}/timeline?limit=50&before=&include_backfill=0&date_field=occurred { items: ChangeEvent[], next_before, date_field, include_backfill } — 1.1: defaults is_backfill=false, ordered by occurred_at; cursor = occurred_at (or observed_at with date_field=observed)
GET /entities/{slug}/history?property= { items: Claim[] } — full claim history (all statuses), newest first
GET /entities/{slug}/asof?date=YYYY-MM-DD { existed: bool, first_seen_at, date, attributes, claims: Claim[] }
GET /entities/{slug}/graph?depth=1&limit=80 { root, nodes: { id, slug, name, entity_type, organization_name }[], edges: { source, target, predicate }[] }
GET /entities/{slug}/sources { items: SourceRef[] }
GET /entities/{slug}/related?limit= { items: EntitySummary[] } (same org / same family (family_id or label) / shared relations)
GET /models?q=&org=&family=&openness=&modality=&status=&min_params=&max_params=&min_context=&year_from=&year_to=&license=&sort=&order=&limit=&offset=&facets=1 `Page<EntitySummary & { identity_confidence, best_price: { input_per_mtok, output_per_mtok, unit, provider: Org, providers, note }
GET /companies?q=&country=&kind=&sort=&limit=&offset=&facets=1 Page<EntitySummary & { model_count, paper_count }> — covers `company
GET /papers?… Page<EntitySummary>
GET /providers `{ items: (EntitySummary & { model_count, price_count, min_input_per_mtok, min_output_per_mtok, input_price_distribution: {min,p25,median,p75,max,n}
GET /prices?model=&provider=&sort=&limit=&offset=&current=1&family=&org=&modality= Page<Price> — 1.1: family, org, modality filters; sort=cheapest_frontier (frontier models only, cheapest output first, + methodology)
GET /prices/history?model=&provider= { items: Price[] }
GET /prices/index?days=180 AI Price Index (1.1) `{ days, series: { day, median_input, median_output, median_frontier_output, median_frontier_input, median_open_output, median_embedding_input, min_input, max_input, min_frontier_output, models, offers, sample: { models, offers, frontier_models, frontier_offers, open_models, embedding_models } }[], movers: ChangeEvent[], cheapest_frontier: { model, provider, output, input, context_length, price_id }
GET /benchmarks?category= `{ items: { id, entity_type, slug, name, category, family, variant, metric, unit, direction, attributes, result_count, model_count, leader: LeaderboardRow
GET /benchmarks/{slug} EntityDetail + family, variant, metric, direction, category, groups, primary_group, result_count, model_count, leaderboard (top 25), trust_mix
GET /benchmarks/{slug}/results?limit=&offset=&config=&history=&metric=&config_key= Page<BenchmarkResult> — v1 behaviour (one row per result) + metric / config_key filters
GET /benchmarks/{slug}/history?model= { benchmark, items: BenchmarkResult[] }
GET /hardware?… Page<EntitySummary> + facets
GET /hardware/fit?memory_gb=&quant=&context=&limit=&openness= v1 estimate (unchanged)
GET /explore/types · GET /explore/{type} unchanged (artifact, model_family appear as types once present); 1.1: framework/library/runtime/tool items expose kind (canonical framework kind, normalised on read, kind_raw when the source label differed) and dataset summaries surface publisher, size, access, hf_repo, modality, license_key… when recorded
GET /changes?category=&type=&entity_type=&importance_min=&since=&until=&q=&entity=&limit=50&before=&offset=&include_documents=0&include_backfill=0&date_field=occurred Page<ChangeEvent> & { next_before, date_field, include_backfill } — 1.1: defaults is_backfill=false, occurred_at; cursor/filters follow date_field
GET /changes/daily?date=&per_section=30&include_backfill=0&date_field=occurred { date, counts, total, sections (v1 by category), today: { key, label, items: (ChangeEvent & { sources: n, documents: string[], grouped_events, event_ids? })[], total }[], new_models, labels, backfill_excluded, previous_day, next_day, note } — Today in AI 2.0 sections: MAJOR_RELEASES, OPEN_WEIGHT_RELEASES, PRICE_MOVES, BENCHMARK_MOVES, MODEL_CHANGES, RESEARCH, DEPRECATIONS, PROVIDER_CHANGES, HARDWARE; events sharing a group_key are folded into one item
GET /changes/categories?days=7&include_backfill=0 { items: { category, event_type, count }[] }
GET /timeline?entity=&year=&category=&importance_min=&limit=&include_backfill=0&date_field=occurred { items: { month, count, events }[], total, date_field, include_backfill }
GET /compare?ids=a,b,…&diff_only=0&mode= { entity_type, dimensions, items, comparability: Record<dimKey, { level, reasons, trust }>, diff_only, note } — 1.1: benchmark dimensions keyed bench:<benchmark>:<metric>:<config_key> (only groups where EVERY model has a current result; dimension carries comparability, trust_levels), model dims add reasoning, tool_calling; diff_only=1 keeps differing dimensions; `mode=models
GET /diff?a=&b=&scope=&limit=&include=&include_backfill=0 { a, b, scope, new_entities, gone_entities, property_changes, price_changes, benchmark_changes, new_benchmark_leaders: { benchmark, at_a, at_b }[], provider_changes, hardware_changes, context_changes, retired_models, counts, include_artifacts, include_backfill, note } — 1.1: events on occurred_at, not back-filled; new_entities excludes artifacts unless include=artifacts
GET /sources unchanged
GET /methodology v1 keys + 1.1: openness: { categories, labels, definitions, dimensions }, trust_levels, comparability (rules, task/condition/ignored keys), counters (how counted), anomaly_checks, event_semantics, hardware_fit, frontier, find_a_model, licence_categories
GET /trending?days=7&limit=12&type=&kind=views { days, kind, items, definition } — 1.1: kind=views (v1, views) · most_changed · new_listings · new_results (each events, last_event_at); separate lists, never merged
POST /views · GET /sitemap · GET /api-keys/me unchanged

# EntityDetail

ts
EntitySummary & {
  attributes: Record<string, unknown>; provenance: Provenance; aliases: string[]; identifiers: { scheme, value }[]
  relations: { predicate, direction: "out"|"in", items: EntitySummary[], total }[]; sources: SourceRef[]; timeline: ChangeEvent[]   // timeline: occurred_at order, not back-filled
  redirected_from?: { slug, id, entity_type }        // 1.1: the caller asked for a merged/folded row — web should 301 to this detail's slug
  // model
  prices?: Price[]; price_history?: Price[]; results?: BenchmarkResult[]; lineage?: { ancestors, descendants, quantizations }; providers?: EntitySummary[]
  hardware_fit?: {…}[]; hardware_fit_assumptions?: string[]; papers?: EntitySummary[]; repositories?: EntitySummary[]
  family?: EntitySummary | { id: null, name, canonical: false, note } | null                                           // 1.1
  artifacts?: { items: { kind, items: (EntitySummary & { artifact_kind })[], count }[], total }                         // 1.1, grouped by artifact_kind
  deployments?: Deployment[]                                                                                           // 1.1
  identity?: { canonical_model: true, official_checkpoints: string[], official_artifacts, third_party_artifacts, provider_deployments, folded_variants, api_aliases: string[], note }
  licence?: LicenseInfo & { raw, url_observed } | { key: null, raw, note }                                            // 1.1
  openness?: { category, raw, label, definition, dimensions: Record<dim, boolean|null>, note }                        // 1.1
  version_history?: { property, transitions: { from, to, valid_from, valid_to, effective_at, source_url, tier, claim_id, status }[], current }[]  // context_length, max_output_tokens, status, knowledge_cutoff, license, openness, parameter_count
  benchmarks?: { items: { id, slug, name, category, metrics: { metric, groups: { config_key, comparability_group, n_rows, higher_is_better, best: {…trust_level…}, trust_levels }[] }[] }[], total_rows, note }
  family_id?: string | null; identity_confidence?: "high"|"medium"|"low"
  // artifact (1.1)
  canonical?: EntitySummary | null; artifact_kind?: "checkpoint"|"quantization"|"conversion"|"packaging"|null
  // provider (1.1)
  deployments?: Deployment[]                        // current offers of this provider, cheapest output first
  removed?: Deployment[]                            // offers closed in the last 90 days (status delisted)
  // company / provider / hardware / framework / model_family
  models?: Page<EntitySummary>; papers?; repositories?
}

# 1.1 additions

route returns
GET /models/{a}/diff/{b} `{ a, b, dimensions: (Dimension & { a, b, delta: { absolute, percent }
`GET /entities/{slug}/claims?property=&status=current all
GET /entities/{slug}/provenance/{property} evidence drawer: { entity, property, value, value_raw, unit, source: { id, name, domain, url }, tier, confidence, extractor, extractor_version, observed_at, effective_at, valid_since, claim_id, run_id, snapshot_id, snapshot: { id, observed_at, document_url, title, archived }, conflicts: Claim[], history_count, note } (404 when no claim and no attribute)
GET /claims/{id} { claim, entity, property, chain: { previous: Claim[], superseding: Claim[], conflicting: Claim[], history_count }, source: { id, name, domain, tier, url, snapshot_id, observed_at }, extractor: { name, version, confidence }, run_id, evidence: { snapshot_id, document_url, archived, snapshot_observed_at, document_title, doc_type }, note }
GET /benchmarks/{slug}/leaderboard?metric=&config_key=&trust=&org=&since=&until=&comparable_only=0&limit=100&offset=0 { benchmark, group: Group, groups: Group[], items: LeaderboardRow[], total, limit, offset, comparable_only, filters, history_available, methodology } — ONE row per canonical model (best current row in the group; default group = primary metric, most-populated config); delta_rank vs the ranking of the closed rows of the same group when history exists
GET /benchmarks/{slug}/frontier?metric=&config_key= { benchmark, series: { group: Group, primary: bool, points: { date, model: ModelRef, score, trust_level, config, result_id }[], current_leader }[], generated_at, methodology } — leader history per group (a point each time a new best appears, ordered by coalesce(evaluated_at, observed_at), closed rows included)
GET /benchmarks/matrix?benchmarks=a,b,c&models=…&org=&family=&since=&until=&limit=60&comparable_only=0&min_cells=3 `{ columns: { id, slug, name, category, metric, config_key, group_label, higher_is_better, n_models }[], rows: { model: {…}, cells: Record<benchmarkId, { score, rank, trust_level, config_key, comparability, result_id, observed_at, evaluated_at }
`GET /deployments?model=&provider=&org=&current=1&status=active delisted
`GET /cost?model=&provider=&input_tokens=1000&output_tokens=500&requests_per_day=1000&cached_share=0..1&batch=0 1`
GET /cost/context?tokens=1000000&limit=50&model=&org= `{ tokens, items: { deployment, context_length, context_source: "offer"
GET /frontier?limit=12 { latest_major_models: ChangeEvent[], benchmark_frontier: { benchmark, group, leader, second, gap }[], price_frontier: { cheapest_output: Deployment, cheapest_output_1m_context, frontier_models, composition }, context_frontier: { model, context_length }[], open_weight_frontier: { items: { model, best_rank, best_rank_on, parameter_count, context_length, ranks }[], dimensions, note }, efficiency_frontier: { quality: { benchmark, group }, x, points: { id, model, x, y, rank, trust_level, pareto }[], frontier: string[] }, agentic_frontier: { benchmark, group, leaders: LeaderboardRow[] }[], multimodal_frontier: { model, modalities, top10_on }[], recent_frontier_movements: (ChangeEvent & { percent_change? })[], generated_at, methodology }
`GET /pareto?benchmark=&x=output_price input_price
GET /pulse?days=7 { days, since, until, counters: Record<key, { value, definition, median_percent?, items? }>, note } — keys: new_models, new_open_weight_models, new_artifacts, new_papers, provider_listings, provider_delistings, price_changes (items: { id, summary, model, provider, occurred_at, percent_change, delta }[], median_percent), new_models_1m_context (items: EntitySummary[]), new_benchmark_leaders (items), documents_changed, sources_observed, events_total; all is_backfill=false, occurred_at in window. Price deltas are read from the events' {input_per_mtok, output_per_mtok} old/new values (output first)
`GET /open?sort=release params
`GET /find-a-model?use_case=coding reasoning
`GET /run-locally?memory_gb=&gpu_count=1 2
GET /hardware/{slug}/fit?quant=&context=&memory_gb=&gpu_count=&limit=&openness= { hardware, memory_options_gb, inputs, estimated: true, assumptions, runtimes, counts, items: ({ model } & Fit)[] }
`GET /families?q=&org=&sort=models name
GET /families/{slug}?limit= { id, slug, name, canonical, summary, …aggregates, members: { model, key_facts, benchmark_ranks }[], artifacts_count, providers, lineage: { source, target, predicate }[], timeline, note } — accepts a family label pre-canonicalisation
`GET /graph/explore?node=&mode=lineage research
`GET /time-machine?date=YYYY-MM-DD&scope=models prices
GET /licenses · GET /licenses/{key}?limit= { items: (LicenseInfo & { aliases, models })[], total, categories, unclassified: { raw, models }[], note } · LicenseInfo & { aliases, models: Page<EntitySummary> }

query = CompiledQuery & { compiled: { filter, label, value, source_span }[], sort, residual, unrecognised: string[], semantic, version: 2, note? }. Recognised: open|open-weights|open source, proprietary|closed, reasoning|thinking models, vision|multimodal|image|audio|video|embedding(s), under/below/cheaper than $X/M [input|output], fit(s) in|runs on N GB, on my mac, context ≥ 1M|1M context|long-context, since|after 2025, in 2026, released this year|last year, last N days|weeks|months, by <Org>|from <Org>|<Org> models… (verified against organization names and aliases — an unknown name is demoted to free text and listed in unrecognised), over|≥|more than 100B, under 30B, between 7B and 70B, apache|mit|bsd|cc-by|gpl|llama|gemma license, commercial use, papers …, benchmark <name>, provider <name>, sort hints cheapest|newest|largest|smallest|best. memory_gb becomes an ESTIMATED parameter bound (4-bit, 8K context) — noted in query.note. All numeric casts in SQL are regex-guarded.

# Semantics changes (1.1)

  1. Models universe. /models, /hardware/fit, /open, /run-locally, leaderboards, matrix, price index and /stats.entities.model count canonical model releases: entity_type = 'model' and merged_into is null. Artifacts (entity_type = 'artifact': checkpoints, quantisations, conversions, packagings) and folded evaluation variants (rows with merged_into) are excluded. include=artifacts on /models restores the pre-1.1 universe. Every old slug keeps resolving: /models/<artifact slug> returns the artifact detail (canonical, artifact_kind); /models/<folded variant slug> returns the canonical model with redirected_from.
  2. Event date fields. /changes, /changes/daily, /changes/categories, /timeline, /entities/{slug}/timeline, /diff, /pulse, /trending?kind≠views use occurred_at = coalesce(effective_at, observed_at) and exclude is_backfill = true rows. include_backfill=1 and date_field=observed restore the v1 behaviour; cursors follow the chosen field. /stats.change_events_24h keeps the v1 meaning (observed) and change_events_live_24h is the new "what happened today" counter.
  3. Leaderboard grouping. Results are organised in comparability groups (canonical metric × config_key, the hash of task-defining configuration keys). /benchmarks leader/top, /benchmarks/{slug}/leaderboard, /benchmarks/matrix, /compare benchmark dimensions, /pareto, /frontier and /find-a-model ranks are computed per group with one row per canonical model. /benchmarks/{slug}/results keeps the v1 one-row-per-result listing. Rows written before migration 0003 (NULL config_key/trust_level) are recomputed on read from config and the source key, so the API behaves identically before and after canonicalisation.

Also: /companies.total == /stats.organizations_total; /benchmarks/{slug} resolves aliases (GPQA Diamond); /entities/{slug}/related uses family_id when present; x-forwarded-for is honoured only when settings.trust_proxy / AIA_TRUST_PROXY=1.

# Deprecations

None. No v1 route, parameter, field or type was removed or retyped in 1.1.

# Admin routes (x-aia-admin-token; rate limit 240/min per IP evaluated BEFORE the token; 10 failed auths/min per IP → 429; every call except the polling GETs overview, infrastructure, llm/health, audit writes an admin_audit_log row { actor: 'admin', action: '<METHOD> <path>', target, payload: { method, path, query?, body? }, ip })

route returns / action
GET /admin/overview · GET /admin/connectors · POST /admin/connectors/{name}/run · PATCH /admin/connectors/{name} · GET /admin/runs · GET /admin/errors v1 (unchanged)
GET /admin/documents… · GET /admin/documents/{id} · GET /admin/snapshots/{id} · GET /admin/jobs · POST /admin/jobs/{id}/retry · POST /admin/jobs/requeue-dead · GET /admin/llm-jobs · GET /admin/llm/health · POST /admin/llm/enqueue · POST /admin/reprocess v1 (unchanged)
GET /admin/review · POST /admin/review/{id} · GET /admin/entities/duplicates?type=&limit=&threshold= · POST /admin/entities/merge · POST /admin/entities/{id}/claims/{claim_id}/retract v1 — 1.1: duplicates no longer calls set_limit() (GUC leak): the % operator uses the index at the default 0.3 threshold and similarity() > :threshold applies the exact bound
GET /admin/infrastructure · POST /admin/stats/recompute · POST /admin/quality/recompute v1 (unchanged)
POST /admin/cache/flush?prefix=&after_run=0 { flushed, prefix }; after_run=1 → { flushed, by_prefix } over the read paths a connector run invalidates (/api/v1/stats, /changes*, /benchmarks*, /prices*, /models*, /deployments, /frontier, /pulse, /open, /families, /timeline, /diff, facets:, frontier:). The scheduler calls services.cache.cache_invalidate(prefix) with the same prefixes after each run (Stream A).
GET /admin/quality data-health dashboard: duplicate_candidates { pending_decisions, review_merge_candidates }, taxonomy_violations { unmapped_taxonomy_rows, openness_unknown_vocab, status_unknown_vocab, license_unclassified }, impossible_values { count, by_check, sample }, conflicting_t1_claims, models_without_organization, models_without_release_source, models_without_parameters, orphan_benchmark_results (model merged or artifact), benchmarks_without_results, unresolved_provider_deployments (provider_model_id matching no identifier), quantisations_typed_as_models (ontology name analysis says artifact), stale_sources (last success > 3× interval), empty_public_categories, quarantined_runs_pending — each { count, sample } — plus review_queue_priority: { kind, id, reasons: string[], … }[] (frontier models, benchmark leaders, largest params/context claims, price anomalies, major orgs, duplicates of frontier models first)
**`GET /admin/entity-resolution?type=model&status=pending decided
POST /admin/entity-resolution/{a}/{b} `{ decision: merge alias
**`GET /admin/anomalies?status=open all&severity=&check=&limit=&offset=** · **POST /admin/anomalies/{id}** { status: resolved
GET /admin/extractions/{snapshot_id}?text_limit= extraction debugger: snapshot metadata (no paths), text (≤ 20 kB cleaned text), structured, diff, previous_snapshot, claims, relations, results, prices, events, llm_jobs, entity_candidates, spans: { claim_id, property, value, found, offset?, match?, context?, tried? }[], spans_found — value location is best effort, not-found is reported
**`GET /admin/quarantine?status=pending all** · **POST /admin/quarantine/{id}** { action: release
GET /admin/audit?limit=&offset=&action= { items: { id, actor, action, target, payload, ip, created_at }[], total }
POST /admin/runs/{run_id}/rollback retracts claims (status = retracted), closes relations / prices / results (valid_to, is_current = false), flags events is_backfill = true + meta.rolled_back, re-materialises attributes from the best remaining claim — only rows with that run_id; deletes nothing → { ok, run_id, connector, counts, note }

# Implementation notes (src/aiatlas/api)

  • Validation errors: parameters rejected by FastAPI/pydantic typing or bounds return 422 with { detail, errors }; semantic errors raised by the routes return 400.
  • Caching: public GETs are cached in Redis (aia:api:<ns>:<path>?<sorted query>, 60–600 s) where <ns> = 8-hex sha1 of DATABASE_URL — two API processes on different databases (dev :8331, prod copy :8332) sharing one Redis db never read each other's bodies or derived id sets (the frontier id set is also re-validated against live canonical models on every cache hit) — and served with a weak ETag (W/"sha1(body)") computed on the uncompressed body inside GZip; /stats stays at 60 s. Cache invalidation: services.cache.cache_invalidate(prefix); POST /admin/cache/flush?after_run=1.
  • Connections: EntityDetail runs its blocks in ≤ 4 concurrent groups (one pooled connection each, sequential inside a group) instead of one connection per block.
  • Performance (2026-09-12, prod copy, EXPLAIN ANALYZE): leaderboard load 8 ms · matrix / all groups 8 ms · models listing 0.5 ms (+ count 1.2 ms, facets ≤ 4 ms) · changes feed 0.8 ms (count 3 ms) · daily sections 0.4 ms · price index 180 d 9 ms · frontier composition 1.2 ms · stats 7 ms · deployments cursor 0.4 ms · providers 2.5 ms · free-text search 16 ms. No query exceeded 200 ms, so no migration 0004 was needed (the 0003 partial indexes on (is_backfill, occurred_at), (benchmark_id, config_key, is_current), family_id, canonical_id are used). The admin trigram self-join (/admin/entities/duplicates, /admin/entity-resolution) takes ~120 ms in SQL + per-pair side-by-side lookups; it is admin-only and not cached.
  • Search: query.semantic says whether an embedding was used; on embedding timeout (2 s) the API backs off to FTS-only for 5 minutes.
  • hardware_fit (model detail) is omitted when parameter_count is unknown; every fit payload carries estimated: true.
  • Rate limits are in-process sliding windows (search 60/min, views 1/s, admin 240/min, admin-auth-failed 10/min per IP) — fine for the single uvicorn worker behind Next.js.
  • Migrations: 0002_api_indexes (read paths), 0003_canonical_ontology (hierarchy, event semantics, comparability, admin tables) — both required by 1.1.