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
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=¤t=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
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=¤t=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> } |
Search compiler v2 (/search)
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)
- Models universe.
/models,/hardware/fit,/open,/run-locally, leaderboards, matrix, price index and/stats.entities.modelcount canonical model releases:entity_type = 'model' and merged_into is null. Artifacts (entity_type = 'artifact': checkpoints, quantisations, conversions, packagings) and folded evaluation variants (rows withmerged_into) are excluded.include=artifactson/modelsrestores 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 withredirected_from. - Event date fields.
/changes,/changes/daily,/changes/categories,/timeline,/entities/{slug}/timeline,/diff,/pulse,/trending?kind≠viewsuseoccurred_at = coalesce(effective_at, observed_at)and excludeis_backfill = truerows.include_backfill=1anddate_field=observedrestore the v1 behaviour; cursors follow the chosen field./stats.change_events_24hkeeps the v1 meaning (observed) andchange_events_live_24his the new "what happened today" counter. - Leaderboard grouping. Results are organised in comparability groups (canonical metric ×
config_key, the hash of task-defining configuration keys)./benchmarksleader/top,/benchmarks/{slug}/leaderboard,/benchmarks/matrix,/comparebenchmark dimensions,/pareto,/frontierand/find-a-modelranks are computed per group with one row per canonical model./benchmarks/{slug}/resultskeeps the v1 one-row-per-result listing. Rows written before migration 0003 (NULLconfig_key/trust_level) are recomputed on read fromconfigand 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 ofDATABASE_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;/statsstays at 60 s. Cache invalidation:services.cache.cache_invalidate(prefix);POST /admin/cache/flush?after_run=1. - Connections:
EntityDetailruns 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_idare 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.semanticsays 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 whenparameter_countis unknown; every fit payload carriesestimated: 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.