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%
11.6 KB · 186 lines tsx
Raw Blame History
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