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 { CONTEXTS, FitForm, QUANTS } from '@/components/hardware/fit-form';6import { Estimated, OpennessBadge } from '@/components/ui/badges';7import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';8import { EntityLink } from '@/components/ui/entity';9import { Container, Note, PageHeader } from '@/components/ui/section';10import { EmptyState, Unavailable } from '@/components/ui/unavailable';11import { api, ApiError } from '@/lib/api';12import { fmtGb, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';13import { routes } from '@/lib/site';14import type { HardwareFit } from '@/lib/types';1516export const revalidate = 600;1718type SP = Record<string, string | undefined>;1920function parseInputs(sp: SP): { memory: number | null; quant: string; context: number; fitsOnly: boolean } {21 const custom = num(sp.memory_custom);22 const preset = num(sp.memory_gb);23 const memory = custom !== null && custom > 0 ? custom : preset !== null && preset > 0 ? preset : null;24 const quant = QUANTS.some((q) => q.value === sp.quant) ? (sp.quant as string) : '4bit';25 const ctx = num(sp.context);26 const context = ctx !== null && ctx > 0 ? Math.round(ctx) : 8192;27 return { memory, quant, context, fitsOnly: sp.fits === '1' };28}2930export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {31 const { memory, quant, context } = parseInputs(await searchParams);32 const base = 'Hardware fit — which models run on my machine? (estimated)';33 const title = memory ? `Models estimated to fit ${fmtGb(memory)} at ${quant}, ${fmtTokens(context)} context` : base;34 return {35 title,36 description: 'Estimate which AI models fit a given amount of device memory at 4-bit, 8-bit or fp16 with a chosen context window. Estimates only — bytes per parameter plus a KV-cache allowance, never a measurement.',37 alternates: { canonical: memory ? routes.hardwareFit({ memory_gb: memory, quant, context }) : '/hardware/fit' },38 };39}4041export default async function HardwareFitPage({ searchParams }: { searchParams: Promise<SP> }) {42 const sp = await searchParams;43 const { memory, quant, context, fitsOnly } = parseInputs(sp);44 let res: HardwareFit | null = null;45 let error: string | null = null;46 if (memory) {47 try {48 res = await api.hardwareFit({ memory_gb: memory, quant, context, limit: 200 });49 } catch (e) {50 error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable';51 }52 }53 const items = res ? (fitsOnly ? res.items.filter((i) => i.fits) : res.items) : [];54 const fits = num(res?.counts?.fits) ?? res?.items.filter((i) => i.fits).length ?? null;55 const evaluated = num(res?.counts?.evaluated) ?? res?.items.length ?? null;56 const self = (patch: { fits?: boolean }) => routes.hardwareFit({ memory_gb: memory ?? undefined, quant, context }) + (patch.fits ? '&fits=1' : '');57 const ctxLabel = CONTEXTS.find((c) => Number(c.value) === context)?.label ?? `${fmtTokens(context)} tokens`;5859 return (60 <Container wide>61 <PageHeader62 eyebrow={63 <>64 <Link href={routes.hardware()} className="hover:text-ink">Hardware</Link> <span aria-hidden>/</span> Fit tool65 </>66 }67 title="What fits my machine?"68 lede="Pick a device memory size, a quantization and a context window: the atlas estimates each model's memory need from its parameter count. Every figure here is an estimate, never a measurement."69 aside={<Estimated />}70 >71 <FitForm memory={memory ? String(memory) : undefined} quant={quant} context={String(context)} className="mt-6" />72 </PageHeader>7374 <div className="pb-16">75 {!memory ? (76 <EmptyState title="Choose a memory size to start">77 Try <Link href={routes.hardwareFit({ memory_gb: 32, quant: '4bit', context: 8192 })} className="link">32 GB · 4-bit · 8k</Link>, <Link href={routes.hardwareFit({ memory_gb: 80, quant: '8bit', context: 32768 })} className="link">80 GB · 8-bit · 32k</Link> or <Link href={routes.hardwareFit({ memory_gb: 192, quant: 'fp16', context: 131072 })} className="link">192 GB · fp16 · 128k</Link>.78 </EmptyState>79 ) : !res ? (80 <Unavailable what="Hardware fit" reason={error ?? undefined} />81 ) : (82 <>83 <div className="mb-5 border-y border-rule py-3">84 <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">85 <Estimated />86 <span className="font-semibold text-ink">All figures are estimates, not measurements.</span>87 <span className="tnum text-ink-2">88 {fmtGb(memory)} · {QUANTS.find((q) => q.value === quant)?.label.split(' (')[0] ?? quant} · {ctxLabel}89 </span>90 {fits !== null && evaluated !== null && (91 <span className="tnum text-ink-3">92 <span className="font-medium text-positive">{fmtInt(fits)}</span> of {fmtInt(evaluated)} models with a known parameter count fit93 </span>94 )}95 </p>96 <ul className="mt-2 list-disc space-y-0.5 pl-5 text-xs leading-relaxed text-ink-3">97 {res.assumptions.map((a) => (98 <li key={a}>{a}</li>99 ))}100 {res.assumptions.length === 0 && <li>Assumptions unavailable from the API.</li>}101 </ul>102 <p className="mt-2 flex flex-wrap items-center gap-3 text-xs">103 {fitsOnly ? (104 <Link href={self({})} className="link">Show all evaluated models</Link>105 ) : (106 <Link href={self({ fits: true })} className="link">Show only fitting models</Link>107 )}108 <Link href="/methodology#estimates" className="text-ink-3 hover:text-ink">Method →</Link>109 </p>110 </div>111112 {items.length === 0 ? (113 <EmptyState title={fitsOnly ? 'No model is estimated to fit' : 'No models to evaluate'}>{fitsOnly ? 'Try a larger memory size or a lower-precision quantization.' : 'Models without a sourced parameter count are not estimated.'}</EmptyState>114 ) : (115 <DataTable caption="Estimated hardware fit">116 <thead>117 <tr>118 <Th>Model</Th>119 <Th num>Params</Th>120 <Th>Quant</Th>121 <Th num>Est. memory</Th>122 <Th num>Headroom</Th>123 <Th>Fits</Th>124 <Th className="w-24"><span className="sr-only">Compare</span></Th>125 </tr>126 </thead>127 <tbody>128 {items.length === 0 && <EmptyRow cols={7} />}129 {items.map((r) => {130 const openness = typeof r.model.attributes?.openness === 'string' ? r.model.attributes.openness : null;131 return (132 <tr key={`${r.model.id}-${r.quantization}`}>133 <Td primary>134 <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5">135 <EntityLink e={r.model} />136 {openness && <OpennessBadge openness={openness} />}137 </span>138 {r.model.organization && <span className="block text-xs text-ink-3">{r.model.organization.name}</span>}139 {r.note && <span className="block text-xs text-ink-3">{r.note}</span>}140 </Td>141 <Td num label="Params" className="tnum">{fmtParams(r.parameter_count)}</Td>142 <Td label="Quant" className="mono text-xs text-ink-2">{r.quantization}</Td>143 <Td num label="Est. memory" className="tnum">{fmtGb(r.estimated_memory_gb, 1)}</Td>144 <Td num label="Headroom" className={r.fits ? 'tnum text-positive' : 'tnum text-danger'}>145 {num(r.headroom_gb) === null ? '—' : `${r.headroom_gb >= 0 ? '+' : '−'}${fmtGb(Math.abs(r.headroom_gb), 1)}`}146 </Td>147 <Td label="Fits" className={r.fits ? 'font-medium text-positive' : 'text-ink-3'}>{r.fits ? '✓ fits' : '✗ too large'}</Td>148 <Td className="text-right">149 <CompareButton e={r.model} size="sm" />150 </Td>151 </tr>152 );153 })}154 </tbody>155 </DataTable>156 )}157 <Note className="mt-3">158 Headroom = device memory − 2 GB reserve − estimated need. Sorted by the API (fitting models first). Compare the models you shortlist with the + buttons. Devices with these memory sizes: <Link href={routes.hardware()} className="link">hardware listing</Link>.159 </Note>160 </>161 )}162 </div>163 <CompareTrayBar />164 </Container>165 );166}167