HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import 'server-only';2import type {3 AsOfPayload,4 BenchmarkResult,5 BenchmarkRow,6 ChangeCategories,7 ChangeEvent,8 Claim,9 CompaniesPage,10 ComparePayload,11 DailyDigest,12 DiffPayload,13 EntityDetail,14 EntitySummary,15 ExploreType,16 FrontierPayload,17 GraphPayload,18 OpenModelsPayload,19 ProvenanceDetail,20 PulsePayload,21 HardwareFit,22 Health,23 Methodology,24 ModelsPage,25 Page,26 Price,27 PriceIndex,28 ProviderRow,29 SearchPayload,30 SitemapPayload,31 SourceRef,32 SourceRow,33 Stats,34 StatsHistory,35 Suggestion,36 TimelinePayload,37 TrendingRow,38} from './types';3940/**41 * Typed fetch wrapper for the AI Atlas API (server components only — client code calls the same-origin `/api/v1/*`42 * rewrite through `src/lib/client-api.ts`).43 *44 * - default cache: ISR `next: { revalidate: 300 }` (5 min); live endpoints pass `revalidate: false` (no-store).45 * - non-2xx → `ApiError` (status + `{detail}` body); network failure → status 0. Pages must render an46 * "Unavailable" state rather than crash (graceful degradation). 404 → `notFound()` in the page.47 * - `safe(promise)` turns any error into `null` for optional panels fetched in parallel.48 */49export const API_URL = (process.env.API_URL ?? 'http://127.0.0.1:8331').replace(/\/$/, '');50const BASE = `${API_URL}/api/v1`;5152export class ApiError extends Error {53 readonly status: number;54 readonly detail: string | null;55 readonly path: string;56 constructor(status: number, path: string, detail: string | null, message?: string) {57 super(message ?? detail ?? `API ${status} on ${path}`);58 this.name = 'ApiError';59 this.status = status;60 this.detail = detail;61 this.path = path;62 }63 get unavailable(): boolean {64 return this.status === 503 || this.status === 0 || this.status >= 500;65 }66 get notFound(): boolean {67 return this.status === 404;68 }69}7071export interface FetchOptions {72 /** Seconds; `false` → `cache: 'no-store'`. Default 300. */73 revalidate?: number | false;74 tags?: string[];75}76export type Query = Record<string, string | number | boolean | null | undefined>;7778function qs(query?: Query): string {79 if (!query) return '';80 const p = new URLSearchParams();81 for (const [k, v] of Object.entries(query)) {82 if (v === undefined || v === null || v === '') continue;83 p.set(k, String(v));84 }85 const s = p.toString();86 return s ? `?${s}` : '';87}8889export async function request<T>(path: string, query?: Query, opts: FetchOptions = {}): Promise<T> {90 const url = `${BASE}${path}${qs(query)}`;91 const init: RequestInit & { next?: { revalidate?: number | false; tags?: string[] } } = { headers: { accept: 'application/json' } };92 if (opts.revalidate === false) init.cache = 'no-store';93 else init.next = { revalidate: opts.revalidate ?? 300, tags: opts.tags };94 let res: Response;95 try {96 res = await fetch(url, init);97 } catch (e) {98 throw new ApiError(0, path, null, `API unreachable: ${(e as Error).message}`);99 }100 if (!res.ok) {101 let detail: string | null = null;102 try {103 const body = (await res.json()) as { detail?: unknown };104 detail = typeof body.detail === 'string' ? body.detail : body.detail ? JSON.stringify(body.detail) : null;105 } catch {106 /* non-JSON error body */107 }108 throw new ApiError(res.status, path, detail);109 }110 return (await res.json()) as T;111}112113export async function safe<T>(p: Promise<T>): Promise<T | null> {114 try {115 return await p;116 } catch {117 return null;118 }119}120121const enc = encodeURIComponent;122123// ---------------------------------------------------------------------------------------------------------- endpoints124export const api = {125 health: () => request<Health>('/health', undefined, { revalidate: false }),126 stats: () => request<Stats>('/stats', undefined, { revalidate: 60 }),127 statsHistory: (days = 90) => request<StatsHistory>('/stats/history', { days }, { revalidate: 900 }),128129 search: (q: string, query: Query = {}) => request<SearchPayload>('/search', { q, ...query }, { revalidate: false }),130 suggest: (q: string) => request<{ items: Suggestion[] }>('/search/suggest', { q }, { revalidate: false }),131132 entity: (slugOrId: string) => request<EntityDetail>(`/entities/${enc(slugOrId)}`, undefined, { revalidate: 120 }),133 /** Type-mounted detail: `/models/<slug>`, `/companies/<slug>`, … (404 if the type does not match). */134 entityOfType: (typePath: string, slug: string) => request<EntityDetail>(`/${typePath}/${enc(slug)}`, undefined, { revalidate: 120 }),135 entityTimeline: (slug: string, limit = 50, before?: string) => request<{ items: ChangeEvent[] }>(`/entities/${enc(slug)}/timeline`, { limit, before }, { revalidate: 120 }),136 entityHistory: (slug: string, property?: string) => request<{ items: Claim[] }>(`/entities/${enc(slug)}/history`, { property }, { revalidate: 300 }),137 entityAsOf: (slug: string, date: string) => request<AsOfPayload>(`/entities/${enc(slug)}/asof`, { date }, { revalidate: 3600 }),138 entityGraph: (slug: string, depth = 1, limit = 80) => request<GraphPayload>(`/entities/${enc(slug)}/graph`, { depth, limit }, { revalidate: 600 }),139 entitySources: (slug: string) => request<{ items: SourceRef[] }>(`/entities/${enc(slug)}/sources`, undefined, { revalidate: 300 }),140 entityRelated: (slug: string, limit = 12) => request<{ items: EntitySummary[] }>(`/entities/${enc(slug)}/related`, { limit }, { revalidate: 600 }),141142 models: (query: Query) => request<ModelsPage>('/models', query, { revalidate: 120 }),143 companies: (query: Query) => request<CompaniesPage>('/companies', query, { revalidate: 300 }),144 papers: (query: Query) => request<Page<EntitySummary>>('/papers', query, { revalidate: 300 }),145 providers: () => request<{ items: ProviderRow[] }>('/providers', undefined, { revalidate: 300 }),146 prices: (query: Query) => request<Page<Price>>('/prices', { current: 1, ...query }, { revalidate: 300 }),147 priceHistory: (query: Query) => request<{ items: Price[] }>('/prices/history', query, { revalidate: 600 }),148 priceIndex: (days = 180) => request<PriceIndex>('/prices/index', { days }, { revalidate: 900 }),149 benchmarks: () => request<{ items: BenchmarkRow[] }>('/benchmarks', undefined, { revalidate: 300 }),150 benchmarkResults: (slug: string, query: Query = {}) => request<Page<BenchmarkResult>>(`/benchmarks/${enc(slug)}/results`, query, { revalidate: 300 }),151 benchmarkHistory: (slug: string, model?: string) => request<{ items: BenchmarkResult[] }>(`/benchmarks/${enc(slug)}/history`, { model }, { revalidate: 600 }),152 hardware: (query: Query) => request<Page<EntitySummary>>('/hardware', query, { revalidate: 600 }),153 hardwareFit: (query: Query) => request<HardwareFit>('/hardware/fit', query, { revalidate: 600 }),154155 exploreTypes: () => request<{ items: ExploreType[] }>('/explore/types', undefined, { revalidate: 300 }),156 explore: (type: string, query: Query) => request<Page<EntitySummary>>(`/explore/${enc(type)}`, query, { revalidate: 300 }),157158 changes: (query: Query) => request<Page<ChangeEvent>>('/changes', query, { revalidate: 60 }),159 changesDaily: (date?: string) => request<DailyDigest>('/changes/daily', { date }, { revalidate: 300 }),160 changesCategories: (days = 7) => request<ChangeCategories>('/changes/categories', { days }, { revalidate: 600 }),161 timeline: (query: Query) => request<TimelinePayload>('/timeline', query, { revalidate: 600 }),162 compare: (ids: string[]) => request<ComparePayload>('/compare', { ids: ids.join(',') }, { revalidate: 300 }),163 diff: (a: string, b: string, scope = 'all') => request<DiffPayload>('/diff', { a, b, scope }, { revalidate: 3600 }),164165 sources: () => request<{ items: SourceRow[] }>('/sources', undefined, { revalidate: 300 }),166 methodology: () => request<Methodology>('/methodology', undefined, { revalidate: 3600 }),167 trending: (days = 7, limit = 12) => request<{ items: TrendingRow[] }>('/trending', { days, limit }, { revalidate: 300 }),168 sitemap: (type?: string, limit = 5000, offset = 0) => request<SitemapPayload>('/sitemap', { type, limit, offset }, { revalidate: 3600 }),169170 // ---- 1.1 additions (endpoints may 404 until the API stream lands — always wrap in `safe()` and provide a fallback)171 frontier: () => request<FrontierPayload>('/frontier', undefined, { revalidate: 300 }),172 pulse: () => request<PulsePayload>('/pulse', undefined, { revalidate: 60 }),173 open: (days = 30, limit = 12) => request<OpenModelsPayload>('/open', { days, limit }, { revalidate: 300 }),174 provenance: (slug: string, property: string) => request<ProvenanceDetail>(`/entities/${enc(slug)}/provenance/${enc(property)}`, undefined, { revalidate: 300 }),175 claim: (id: string) => request<Claim & { entity?: EntitySummary }>(`/claims/${enc(id)}`, undefined, { revalidate: 3600 }),176};177178/** Total number of items of one type, from `/stats` (preferred) or a 1-item listing. Null when unavailable. */179export async function countOfType(type: string): Promise<number | null> {180 const s = await safe(api.stats());181 const v = s?.entities?.[type];182 if (v === undefined || v === null) return null;183 const n = Number(v);184 return Number.isFinite(n) ? n : null;185}186187// ---- D1 (models/benchmarks/compare) ----188import type {189 BenchmarkDetail,190 BenchmarkFrontierPayload,191 BenchmarksPayload,192 ComparePayload11,193 FamiliesPage,194 FamilyDetail,195 LeaderboardPayload,196 LicenseDetail,197 LicensesPayload,198 MatrixPayload,199 Methodology11,200 ModelDetail,201 ModelDiffPayload,202 ModelsPage11,203 ParetoPayload,204} from './types';205206/** 1.1 endpoints used by the models · benchmarks · compare · families · licences pages (docs/API.md §1.1). */207export const apiD1 = {208 /** `/models` with the 1.1 facets (families, canonical licences, trust) and `include=artifacts`. */209 models: (query: Query) => request<ModelsPage11>('/models', query, { revalidate: 120 }),210 /** `/models/<slug>` — accepts models AND artifacts; folded variants come back as the canonical model with `redirected_from`. */211 model: (slug: string) => request<ModelDetail>(`/models/${enc(slug)}`, undefined, { revalidate: 120 }),212 modelDiff: (a: string, b: string) => request<ModelDiffPayload>(`/models/${enc(a)}/diff/${enc(b)}`, undefined, { revalidate: 300 }),213 compare: (ids: string[], opts: { diff_only?: boolean; mode?: string } = {}) => request<ComparePayload11>('/compare', { ids: ids.join(','), diff_only: opts.diff_only ? 1 : undefined, mode: opts.mode }, { revalidate: 300 }),214 benchmarks: (category?: string) => request<BenchmarksPayload>('/benchmarks', { category }, { revalidate: 300 }),215 benchmark: (slug: string) => request<BenchmarkDetail>(`/benchmarks/${enc(slug)}`, undefined, { revalidate: 120 }),216 leaderboard: (slug: string, query: Query = {}) => request<LeaderboardPayload>(`/benchmarks/${enc(slug)}/leaderboard`, query, { revalidate: 300 }),217 frontier: (slug: string, query: Query = {}) => request<BenchmarkFrontierPayload>(`/benchmarks/${enc(slug)}/frontier`, query, { revalidate: 600 }),218 matrix: (query: Query = {}) => request<MatrixPayload>('/benchmarks/matrix', query, { revalidate: 600 }),219 pareto: (query: Query) => request<ParetoPayload>('/pareto', query, { revalidate: 600 }),220 families: (query: Query = {}) => request<FamiliesPage>('/families', query, { revalidate: 600 }),221 family: (slug: string, limit = 200) => request<FamilyDetail>(`/families/${enc(slug)}`, { limit }, { revalidate: 600 }),222 licenses: () => request<LicensesPayload>('/licenses', undefined, { revalidate: 3600 }),223 license: (key: string, limit = 100, offset = 0) => request<LicenseDetail>(`/licenses/${enc(key)}`, { limit, offset }, { revalidate: 3600 }),224 methodology: () => request<Methodology11>('/methodology', undefined, { revalidate: 3600 }),225};226// ---- /D1 ----227228// ---- D2 (intelligence) ----229import type {230 CostContextPayload,231 CostPayload,232 DeploymentsPage,233 FinderPayload,234 FrontierIntel,235 HardwarePage,236 HardwareSlugFit,237 MethodologyIntel,238 OpenPayload,239 PriceIndexIntel,240 ProviderIntelRow,241 PulseIntel,242 RunLocallyPayload,243} from './types';244245/** 1.1 intelligence routes, typed for the D2 pages (frontier · prices · calculator · run-locally · find-a-model · open · pulse · providers · hardware). */246export const intel = {247 frontier: (limit = 12) => request<FrontierIntel>('/frontier', { limit }, { revalidate: 300 }),248 /** `/pareto` is typed in the D1 block: `apiD1.pareto(query)`. */249 priceIndex: (days = 180) => request<PriceIndexIntel>('/prices/index', { days }, { revalidate: 900 }),250 prices: (query: Query) => request<Page<Price> & { methodology?: string }>('/prices', { current: 1, ...query }, { revalidate: 300 }),251 providers: () => request<{ items: ProviderIntelRow[]; note?: string }>('/providers', undefined, { revalidate: 300 }),252 deployments: (query: Query) => request<DeploymentsPage>('/deployments', query, { revalidate: 300 }),253 cost: (query: Query) => request<CostPayload>('/cost', query, { revalidate: 300 }),254 costContext: (query: Query) => request<CostContextPayload>('/cost/context', query, { revalidate: 300 }),255 runLocally: (query: Query) => request<RunLocallyPayload>('/run-locally', query, { revalidate: 600 }),256 hardware: (query: Query) => request<HardwarePage>('/hardware', { facets: 1, ...query }, { revalidate: 600 }),257 hardwareSlugFit: (slug: string, query: Query = {}) => request<HardwareSlugFit>(`/hardware/${enc(slug)}/fit`, query, { revalidate: 600 }),258 findAModel: (query: Query) => request<FinderPayload>('/find-a-model', query, { revalidate: 300 }),259 open: (query: Query) => request<OpenPayload>('/open', query, { revalidate: 300 }),260 pulse: (days = 7) => request<PulseIntel>('/pulse', { days }, { revalidate: 120 }),261 methodology: () => request<MethodologyIntel>('/methodology', undefined, { revalidate: 3600 }),262};263// ---- /D2 ----264265// ---- D3 (temporal/graph/admin) ----266import type { ChangesPage, ClaimDetail, DailyDigest2, DiffPayload11, EntityClaimsPayload, GraphExploreMode, GraphExplorePayload, MethodologyD3, SourcesPayload, TimeMachinePayload, TimelinePayload11, TrendingPayload, SearchPayload2 } from './types';267268export const apiD3 = {269 /** Typed neighbourhood explorer (7 modes). Never more than `limit` nodes; `truncated` says when the API cut. */270 graphExplore: (node: string, mode: GraphExploreMode, depth: 1 | 2 = 1, limit = 150) => request<GraphExplorePayload>('/graph/explore', { node, mode, depth, limit }, { revalidate: 600 }),271 timeMachine: (date: string, scope: string, limit = 50) => request<TimeMachinePayload>('/time-machine', { date, scope, limit }, { revalidate: 3600 }),272 diff: (a: string, b: string, scope = 'all', limit = 200, includeBackfill = false) => request<DiffPayload11>('/diff', { a, b, scope, limit, include_backfill: includeBackfill ? 1 : undefined }, { revalidate: 1800 }),273 changesDaily: (date?: string, perSection = 30, includeBackfill = false) => request<DailyDigest2>('/changes/daily', { date, per_section: perSection, include_backfill: includeBackfill ? 1 : undefined }, { revalidate: 300 }),274 changes: (query: Query) => request<ChangesPage>('/changes', query, { revalidate: 60 }),275 timeline: (query: Query) => request<TimelinePayload11>('/timeline', query, { revalidate: 600 }),276 search: (q: string, query: Query = {}) => request<SearchPayload2>('/search', { q, ...query }, { revalidate: false }),277 claim: (id: string) => request<ClaimDetail>(`/claims/${enc(id)}`, undefined, { revalidate: 600 }),278 entityClaims: (slug: string, query: Query = {}) => request<EntityClaimsPayload>(`/entities/${enc(slug)}/claims`, query, { revalidate: 300 }),279 methodology: () => request<MethodologyD3>('/methodology', undefined, { revalidate: 3600 }),280 sources: () => request<SourcesPayload>('/sources', undefined, { revalidate: 300 }),281 trending: (kind: string, query: Query = {}) => request<TrendingPayload>('/trending', { kind, ...query }, { revalidate: 300 }),282 /** Generic entity timeline with the 1.1 flags (`include_backfill`, `date_field`). */283 entityTimeline: (slug: string, query: Query = {}) => request<ChangesPage>(`/entities/${enc(slug)}/timeline`, query, { revalidate: 120 }),284};285// ---- /D3 ----286