import type { Metadata } from 'next'; import Link from 'next/link'; import { CompareButton } from '@/components/compare/compare-button'; import { CompareTrayBar } from '@/components/compare/compare-tray-bar'; import { BTN_GHOST, CTRL, Field, Methodology, RangeBar, SortTh, distDomain } from '@/components/intelligence/bits'; import { TerminalLayout } from '@/components/layout/terminal'; import { Chip } from '@/components/ui/badges'; import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table'; import { EntityLink, QualityMark } from '@/components/ui/entity'; import { withParams } from '@/components/ui/pagination'; import { Note, PageHeader } from '@/components/ui/section'; import { EmptyState, Unavailable } from '@/components/ui/unavailable'; import { intel, safe } from '@/lib/api'; import { fmtAgo, fmtInt, fmtSigned, fmtUsdPerM, num } from '@/lib/format'; import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; import type { ProviderIntelRow } from '@/lib/types'; export const revalidate = 300; type SP = Record; const SORTS = ['models', 'orgs', 'median_input', 'median_output', 'changes', 'added', 'name', 'updated'] as const; type Sort = (typeof SORTS)[number]; const SORT_LABELS: Record = { models: 'Models served', orgs: 'Organizations covered', median_input: 'Median input price', median_output: 'Median output price', changes: 'Price changes · 30 d', added: 'Models added · 30 d', name: 'Name', updated: 'Recently updated' }; const KEYS = ['sort', 'feature', 'min_models', 'q'] as const; const FEATURE_LABELS: Record = { batch: 'Batch', cached: 'Prompt caching', fine_tuning: 'Fine-tuning', audio: 'Audio', image: 'Image', video: 'Video', web_search: 'Web search', flex: 'Flex tier', long_context: 'Long-context tier', priority: 'Priority tier', reasoning: 'Reasoning tokens' }; const TITLE = 'AI inference providers — prices, coverage and changes'; const DESC = 'Every inference provider the atlas tracks: models served, organizations covered, input and output price distributions (min · p25 · median · p75 · max, USD per 1M tokens), price changes and models added or removed in the last 30 days, and the features each provider prices (batch, caching, fine-tuning…).'; export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const sp = await searchParams; const filtered = KEYS.some((k) => sp[k] && k !== 'sort'); return { title: TITLE, description: DESC, alternates: { canonical: routes.providers() }, openGraph: { title: `${TITLE} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.providers()}`, siteName: SITE_NAME }, robots: filtered ? { index: false, follow: true } : undefined }; } const med = (d: ProviderIntelRow['input_price_distribution']) => num(d?.median); export default async function ProvidersPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const cur: Record = {}; for (const k of KEYS) if (sp[k]) cur[k] = sp[k]; const sort: Sort = (SORTS as readonly string[]).includes(cur.sort ?? '') ? (cur.sort as Sort) : 'models'; const res = await safe(intel.providers()); const all = res?.items ?? []; const features = [...new Set(all.flatMap((p) => p.features_supported ?? []))].sort(); const minModels = num(cur.min_models); const q = cur.q?.trim().toLowerCase(); let items = all.filter((p) => (!cur.feature || (p.features_supported ?? []).includes(cur.feature)) && (minModels === null || (num(p.model_count) ?? 0) >= minModels) && (!q || p.name.toLowerCase().includes(q) || p.organization?.name.toLowerCase().includes(q))); const by = (f: (p: ProviderIntelRow) => number | null, dir: 1 | -1 = -1) => (a: ProviderIntelRow, b: ProviderIntelRow) => { const x = f(a); const y = f(b); if (x === null && y === null) return 0; if (x === null) return 1; if (y === null) return -1; return (x - y) * dir; }; items = items.slice().sort( sort === 'name' ? (a, b) => a.name.localeCompare(b.name) : sort === 'updated' ? (a, b) => b.updated_at.localeCompare(a.updated_at) : sort === 'orgs' ? by((p) => num(p.organizations_covered)) : sort === 'median_input' ? by((p) => med(p.input_price_distribution), 1) : sort === 'median_output' ? by((p) => med(p.output_price_distribution), 1) : sort === 'changes' ? by((p) => num(p.price_changes_30d)) : sort === 'added' ? by((p) => num(p.models_added_30d)) : by((p) => num(p.model_count)), ); const domainIn = distDomain(all.map((p) => p.input_price_distribution)); const domainOut = distDomain(all.map((p) => p.output_price_distribution)); const href = (patch: Record) => withParams('/providers', cur, patch); const filterCount = ['feature', 'min_models', 'q'].filter((k) => cur[k]).length; const filters = (
Reset
); const inspector = (

{res?.note ?? 'Aggregates unavailable.'}

Range bars share one logarithmic axis per column (input {fmtUsdPerM(domainIn[0])} → {fmtUsdPerM(domainIn[1])}; output {fmtUsdPerM(domainOut[0])} → {fmtUsdPerM(domainOut[1])}): the light band spans min → max, the solid band p25 → p75, the tick is the median.

Features = keys priced on at least one offer (batch, cached input, fine-tuning, audio…), as published. GET /providers

); return ( {fmtInt(items.length)} of {fmtInt(all.length)} providers

: undefined} className="pt-4 md:pt-6" />
{!res ? ( ) : items.length === 0 ? ( Remove the feature or model-count filter. ) : ( <> Provider Models Orgs Input / 1M · distribution Output / 1M · distribution Δ price · 30 d ± models · 30 d Features Updated Quality {items.length === 0 && } {items.map((p) => ( {p.organization && {p.organization.name}} {fmtInt(p.model_count)} {fmtInt(p.organizations_covered)} {fmtInt(p.price_changes_30d)} {fmtSigned(p.models_added_30d)} / {num(p.models_removed_30d) ? `−${fmtInt(p.models_removed_30d)}` : '0'} {(p.features_supported ?? []).slice(0, 6).map((f) => ( {FEATURE_LABELS[f] ?? f.replace(/_/g, ' ')} ))} {(p.features_supported?.length ?? 0) > 6 && +{(p.features_supported?.length ?? 0) - 6}} {!(p.features_supported?.length ?? 0) && —} {fmtAgo(p.updated_at)} ))} Open a provider for its models-served table with price history, listings and delistings, price events and priced features. Prices are USD per 1M tokens as published. )}
); }