HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { CompareButton } from '@/components/compare/compare-button';4import { CompareTrayBar } from '@/components/compare/compare-tray-bar';5import { BTN_GHOST, CTRL, Field, Methodology, RangeBar, SortTh, distDomain } from '@/components/intelligence/bits';6import { TerminalLayout } from '@/components/layout/terminal';7import { Chip } from '@/components/ui/badges';8import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';9import { EntityLink, QualityMark } from '@/components/ui/entity';10import { withParams } from '@/components/ui/pagination';11import { Note, PageHeader } from '@/components/ui/section';12import { EmptyState, Unavailable } from '@/components/ui/unavailable';13import { intel, safe } from '@/lib/api';14import { fmtAgo, fmtInt, fmtSigned, fmtUsdPerM, num } from '@/lib/format';15import { routes, SITE_NAME, SITE_URL } from '@/lib/site';16import type { ProviderIntelRow } from '@/lib/types';1718export const revalidate = 300;19type SP = Record<string, string | undefined>;20const SORTS = ['models', 'orgs', 'median_input', 'median_output', 'changes', 'added', 'name', 'updated'] as const;21type Sort = (typeof SORTS)[number];22const SORT_LABELS: Record<Sort, string> = { 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' };23const KEYS = ['sort', 'feature', 'min_models', 'q'] as const;24const FEATURE_LABELS: Record<string, string> = { 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' };2526const TITLE = 'AI inference providers — prices, coverage and changes';27const 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…).';28export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {29 const sp = await searchParams;30 const filtered = KEYS.some((k) => sp[k] && k !== 'sort');31 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 };32}3334const med = (d: ProviderIntelRow['input_price_distribution']) => num(d?.median);3536export default async function ProvidersPage({ searchParams }: { searchParams: Promise<SP> }) {37 const sp = await searchParams;38 const cur: Record<string, string | undefined> = {};39 for (const k of KEYS) if (sp[k]) cur[k] = sp[k];40 const sort: Sort = (SORTS as readonly string[]).includes(cur.sort ?? '') ? (cur.sort as Sort) : 'models';41 const res = await safe(intel.providers());42 const all = res?.items ?? [];43 const features = [...new Set(all.flatMap((p) => p.features_supported ?? []))].sort();44 const minModels = num(cur.min_models);45 const q = cur.q?.trim().toLowerCase();46 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)));47 const by = (f: (p: ProviderIntelRow) => number | null, dir: 1 | -1 = -1) => (a: ProviderIntelRow, b: ProviderIntelRow) => {48 const x = f(a);49 const y = f(b);50 if (x === null && y === null) return 0;51 if (x === null) return 1;52 if (y === null) return -1;53 return (x - y) * dir;54 };55 items = items.slice().sort(56 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)),57 );58 const domainIn = distDomain(all.map((p) => p.input_price_distribution));59 const domainOut = distDomain(all.map((p) => p.output_price_distribution));60 const href = (patch: Record<string, string | number | undefined | null>) => withParams('/providers', cur, patch);61 const filterCount = ['feature', 'min_models', 'q'].filter((k) => cur[k]).length;6263 const filters = (64 <form action="/providers" method="get" className="space-y-3" data-provider-filters>65 <Field label="Name">66 <input name="q" defaultValue={cur.q ?? ''} placeholder="Provider or organization" className={CTRL} />67 </Field>68 <Field label="Priced feature">69 <select name="feature" defaultValue={cur.feature ?? ''} className={CTRL}>70 <option value="">Any</option>71 {features.map((f) => (72 <option key={f} value={f}>73 {FEATURE_LABELS[f] ?? f.replace(/_/g, ' ')}74 </option>75 ))}76 </select>77 </Field>78 <Field label="Models served ≥">79 <input name="min_models" inputMode="numeric" defaultValue={cur.min_models ?? ''} placeholder="e.g. 20" className={CTRL} />80 </Field>81 <Field label="Sort">82 <select name="sort" defaultValue={sort} className={CTRL}>83 {SORTS.map((s) => (84 <option key={s} value={s}>85 {SORT_LABELS[s]}86 </option>87 ))}88 </select>89 </Field>90 <div className="flex gap-2">91 <button type="submit" className="inline-flex h-11 flex-1 items-center justify-center bg-ink lg:h-10 px-3 text-sm font-medium text-canvas hover:opacity-90">92 Apply93 </button>94 <Link href="/providers" className={BTN_GHOST}>95 Reset96 </Link>97 </div>98 </form>99 );100101 const inspector = (102 <div className="space-y-3 text-xs leading-relaxed text-ink-2">103 <p>{res?.note ?? 'Aggregates unavailable.'}</p>104 <p>105 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.106 </p>107 <p>108 Features = keys priced on at least one offer (batch, cached input, fine-tuning, audio…), as published. <Link href="/developers" className="link">GET /providers</Link>109 </p>110 </div>111 );112113 return (114 <TerminalLayout filters={filters} inspector={inspector} filtersTitle="Filters" inspectorTitle="Reading" storageKey="aia-inspector-providers" filterCount={filterCount}>115 <PageHeader eyebrow="Providers & pricing" title="Inference providers" lede="Who serves which models, at what published prices, and how those prices move. Distributions are over live offers with a positive price; every change is kept." aside={res ? <p className="tnum text-sm text-ink-3">{fmtInt(items.length)} of {fmtInt(all.length)} providers</p> : undefined} className="pt-4 md:pt-6" />116 <div className="pb-16">117 {!res ? (118 <Unavailable what="Providers" />119 ) : items.length === 0 ? (120 <EmptyState title="No provider matches these filters">Remove the feature or model-count filter.</EmptyState>121 ) : (122 <>123 <DataTable scroll compact>124 <thead>125 <tr>126 <SortTh active={sort === 'name'} href={href({ sort: 'name' })}>Provider</SortTh>127 <SortTh active={sort === 'models'} href={href({ sort: undefined })} num dir="desc">Models</SortTh>128 <SortTh active={sort === 'orgs'} href={href({ sort: 'orgs' })} num dir="desc">Orgs</SortTh>129 <SortTh active={sort === 'median_input'} href={href({ sort: 'median_input' })}>Input / 1M · distribution</SortTh>130 <SortTh active={sort === 'median_output'} href={href({ sort: 'median_output' })}>Output / 1M · distribution</SortTh>131 <SortTh active={sort === 'changes'} href={href({ sort: 'changes' })} num dir="desc">Δ price · 30 d</SortTh>132 <SortTh active={sort === 'added'} href={href({ sort: 'added' })} num dir="desc">± models · 30 d</SortTh>133 <Th>Features</Th>134 <SortTh active={sort === 'updated'} href={href({ sort: 'updated' })} dir="desc">Updated</SortTh>135 <Th num>Quality</Th>136 <Th className="w-24" aria-label="Compare" />137 </tr>138 </thead>139 <tbody>140 {items.length === 0 && <EmptyRow cols={11} />}141 {items.map((p) => (142 <tr key={p.id} data-provider-row>143 <Td primary>144 <EntityLink e={p} />145 {p.organization && <span className="ml-2 text-xs text-ink-3">{p.organization.name}</span>}146 </Td>147 <Td num label="Models" className="tnum">{fmtInt(p.model_count)}</Td>148 <Td num label="Organizations" className="tnum text-ink-2">{fmtInt(p.organizations_covered)}</Td>149 <Td label="Input distribution">150 <RangeBar d={p.input_price_distribution} domain={domainIn} label="Input price" />151 </Td>152 <Td label="Output distribution">153 <RangeBar d={p.output_price_distribution} domain={domainOut} label="Output price" />154 </Td>155 <Td num label="Price changes 30 d" className="tnum">{fmtInt(p.price_changes_30d)}</Td>156 <Td num label="Models ± 30 d" className="tnum">157 <span className="text-positive">{fmtSigned(p.models_added_30d)}</span> <span className="text-ink-3">/</span> <span className={num(p.models_removed_30d) ? 'text-danger' : 'text-ink-3'}>{num(p.models_removed_30d) ? `−${fmtInt(p.models_removed_30d)}` : '0'}</span>158 </Td>159 <Td label="Features" className="max-w-[16rem]">160 <span className="flex flex-wrap gap-1">161 {(p.features_supported ?? []).slice(0, 6).map((f) => (162 <Chip key={f}>{FEATURE_LABELS[f] ?? f.replace(/_/g, ' ')}</Chip>163 ))}164 {(p.features_supported?.length ?? 0) > 6 && <span className="text-[11px] text-ink-3">+{(p.features_supported?.length ?? 0) - 6}</span>}165 {!(p.features_supported?.length ?? 0) && <span className="text-xs text-ink-3">—</span>}166 </span>167 </Td>168 <Td label="Updated" className="text-ink-2 whitespace-nowrap" title={p.updated_at}>{fmtAgo(p.updated_at)}</Td>169 <Td num label="Quality"><QualityMark q={p.quality?.score} /></Td>170 <Td className="text-right">171 <CompareButton e={p} size="sm" />172 </Td>173 </tr>174 ))}175 </tbody>176 </DataTable>177 <Note className="mt-3">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.</Note>178 <Methodology text={res.note} />179 </>180 )}181 </div>182 <CompareTrayBar />183 </TerminalLayout>184 );185}186