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 { QUANTS } from '@/components/hardware/fit-form';6import { EstimateBanner, FitBreakdownList, Methodology, SourceTag } from '@/components/intelligence/bits';7import { CONTEXT_PRESETS, PLATFORMS, RunLocallyForm, type RunLocallyInputs, USE_CASES } from '@/components/intelligence/run-locally-form';8import { TerminalLayout } from '@/components/layout/terminal';9import { Chip, Estimated, OpennessBadge } from '@/components/ui/badges';10import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';11import { EntityLink } from '@/components/ui/entity';12import { Note, PageHeader } from '@/components/ui/section';13import { EmptyState, Unavailable } from '@/components/ui/unavailable';14import { ApiError, intel, safe } from '@/lib/api';15import { fmtGb, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';16import { routes, SITE_NAME, SITE_URL } from '@/lib/site';17import type { Fit, RunLocallyItem, RunLocallyPayload } from '@/lib/types';1819export const revalidate = 600;20type SP = Record<string, string | undefined>;2122function parse(sp: SP): RunLocallyInputs {23 const custom = num(sp.memory_custom);24 const preset = num(sp.memory_gb);25 const memory = custom !== null && custom > 0 ? custom : preset !== null && preset > 0 ? preset : null;26 const gpu = num(sp.gpu_count);27 const ctx = num(sp.context);28 const batch = num(sp.batch);29 return {30 memory,31 gpuCount: gpu !== null && [1, 2, 4, 8].includes(gpu) ? gpu : 1,32 quant: QUANTS.some((q) => q.value === sp.quant) ? (sp.quant as string) : '4bit',33 context: ctx !== null && ctx > 0 ? Math.round(ctx) : 8192,34 batch: batch !== null && batch > 0 ? Math.round(batch) : 1,35 platform: PLATFORMS.some((p) => p.value === sp.platform) ? (sp.platform as string) : 'any',36 useCase: USE_CASES.some((u) => u.value === sp.use_case) ? (sp.use_case as string) : '',37 openness: ['open-source', 'open-weights', 'restricted-weights'].includes(sp.openness ?? '') ? (sp.openness as string) : '',38 hardware: sp.hardware?.trim() || null,39 fitsOnly: sp.fits === '1',40 };41}4243const TITLE = 'Run locally — which AI models fit your machine? (estimated)';44const DESC = 'Local AI Explorer: choose memory, GPU count, platform, quantization, context and batch; the atlas estimates which downloadable models fit — with the weight, KV-cache and overhead breakdown — and lists their compatible GGUF/MLX artifacts, using observed file sizes where a source records them. Every figure is an estimate.';45export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {46 const v = parse(await searchParams);47 const title = v.hardware ? `What can ${v.hardware} run? (estimated)` : v.memory ? `Models estimated to fit ${fmtGb(v.memory * v.gpuCount)} at ${v.quant}, ${fmtTokens(v.context)} context` : TITLE;48 return { title, description: DESC, alternates: { canonical: routes.runLocally() }, openGraph: { title: `${title} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.runLocally()}`, siteName: SITE_NAME }, robots: v.memory || v.hardware ? { index: false, follow: true } : undefined };49}5051export default async function RunLocallyPage({ searchParams }: { searchParams: Promise<SP> }) {52 const sp = await searchParams;53 const v = parse(sp);54 let res: RunLocallyPayload | null = null;55 let hardwareName: string | null = null;56 let memoryOptions: number[] | undefined;57 let error: string | null = null;58 let notFound = false;59 if (v.hardware) {60 try {61 const h = await intel.hardwareSlugFit(v.hardware, { quant: v.quant, context: v.context, memory_gb: v.memory ?? undefined, gpu_count: v.gpuCount, openness: v.openness || undefined, limit: 300 });62 hardwareName = h.hardware.name;63 memoryOptions = h.memory_options_gb;64 res = { inputs: h.inputs, estimated: h.estimated, assumptions: h.assumptions, counts: h.counts, note: h.note ?? `Runtimes listed for this device: ${h.runtimes.join(', ') || 'unavailable'}.`, items: h.items.map(({ model, ...fit }) => ({ model, fit: fit as Fit, artifacts: [], artifact_count: 0 })) };65 } catch (e) {66 if (e instanceof ApiError && e.notFound) notFound = true;67 else error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable';68 }69 } else if (v.memory) {70 try {71 res = await intel.runLocally({ memory_gb: v.memory, gpu_count: v.gpuCount, quant: v.quant, context: v.context, batch: v.batch, platform: v.platform, use_case: v.useCase || undefined, openness: v.openness || undefined, limit: 300 });72 } catch (e) {73 error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable';74 }75 }76 const methodology = res ? null : await safe(intel.methodology());77 const items: RunLocallyItem[] = res ? (v.fitsOnly ? res.items.filter((i) => i.fit.fits) : res.items) : [];78 const multiNote = v.gpuCount > 1 ? res?.items.find((i) => i.fit.multi_gpu_note)?.fit.multi_gpu_note ?? res?.note : null;79 const total = v.memory ? v.memory * v.gpuCount : num(res?.inputs?.total_memory_gb);80 const ctxLabel = CONTEXT_PRESETS.find((c) => c.value === v.context)?.label ?? fmtTokens(v.context);81 const filterCount = [v.memory, v.gpuCount !== 1, v.quant !== '4bit', v.context !== 8192, v.batch !== 1, v.platform !== 'any', v.useCase, v.openness, v.fitsOnly].filter(Boolean).length;82 const withArtifacts = items.filter((i) => i.artifacts.length > 0).length;8384 const inspector = (85 <div className="space-y-3 text-xs leading-relaxed text-ink-2">86 <p className="flex items-center gap-2">87 <Estimated /> <span className="font-medium text-ink">Method</span>88 </p>89 <ul className="list-disc space-y-1 pl-4">90 {(res?.assumptions ?? methodology?.hardware_fit?.assumptions ?? []).map((a) => (91 <li key={a}>{a}</li>92 ))}93 </ul>94 {methodology?.hardware_fit?.bytes_per_param && !res && (95 <p>96 bytes / param:{' '}97 {Object.entries(methodology.hardware_fit.bytes_per_param)98 .map(([k, b]) => `${k} ${b}`)99 .join(' · ')}100 </p>101 )}102 <p>103 <Link href="/methodology#estimates" className="link">/methodology</Link> · <Link href="/developers" className="link">GET /run-locally</Link>104 </p>105 </div>106 );107108 return (109 <TerminalLayout filters={<RunLocallyForm v={v} hardwareName={hardwareName} memoryOptions={memoryOptions} />} inspector={inspector} filtersTitle="Machine" inspectorTitle="Method" storageKey="aia-inspector-run-locally" filterCount={filterCount}>110 <PageHeader eyebrow={v.hardware ? 'What can this machine run?' : 'Local AI Explorer'} title={hardwareName ? `What can ${hardwareName} run?` : 'Run locally'} lede="Describe the machine — memory, number of devices, platform, quantization, context, batch — and the atlas lists the downloadable models estimated to fit, with the breakdown behind each number and the GGUF / MLX artifacts recorded for them. Estimates, never measurements." aside={<Estimated />} className="pt-4 md:pt-6" />111112 <div className="pb-16">113 {notFound ? (114 <EmptyState title={`No hardware with slug “${v.hardware}”`}>115 Pick a device from the <Link href={routes.hardware()} className="link">hardware listing</Link> or describe the machine in the rail.116 </EmptyState>117 ) : !v.memory && !v.hardware ? (118 <EmptyState title="Describe a machine to start">119 Try <Link href="/run-locally?memory_gb=64&quant=4bit" className="link">64 GB · 4-bit</Link>, <Link href="/run-locally?memory_gb=24&gpu_count=2&quant=8bit&platform=nvidia" className="link">2 × 24 GB · 8-bit · NVIDIA</Link>, <Link href="/run-locally?memory_gb=128&quant=4bit&context=131072&use_case=coding&platform=apple" className="link">128 GB · 4-bit · 128K · coding</Link> — or start from a device on <Link href={routes.hardware()} className="link">/hardware</Link>.120 </EmptyState>121 ) : !res ? (122 <Unavailable what="Local fit estimate" reason={error ?? undefined} />123 ) : (124 <>125 <EstimateBanner assumptions={res.assumptions} note={res.note} counts={res.counts} />126 <p className="tnum mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-ink-2">127 <span className="font-medium text-ink">{fmtGb(total)} total</span>128 <span>129 {v.gpuCount} × {fmtGb(v.memory ?? num(res.inputs?.memory_gb))}130 </span>131 <span>{QUANTS.find((q) => q.value === v.quant)?.label.split(' (')[0] ?? v.quant}</span>132 <span>{ctxLabel} context</span>133 <span>batch {v.batch}</span>134 {v.platform !== 'any' && <span>{PLATFORMS.find((p) => p.value === v.platform)?.label.split(' (')[0]}</span>}135 {v.useCase && <Chip tone="accent">{USE_CASES.find((u) => u.value === v.useCase)?.label}</Chip>}136 <span className="ml-auto text-xs text-ink-3">137 {v.fitsOnly ? <Link href={`?${new URLSearchParams(Object.entries(sp).filter(([k, x]) => x && k !== 'fits') as [string, string][]).toString()}`} className="link">Show all evaluated models</Link> : <Link href={`?${new URLSearchParams({ ...(Object.fromEntries(Object.entries(sp).filter(([, x]) => x)) as Record<string, string>), fits: '1' }).toString()}`} className="link">Only fitting models</Link>}138 </span>139 </p>140 {multiNote && v.gpuCount > 1 && (141 <Note className="mt-2 border-l-2 border-warning pl-3">142 <span className="font-medium text-warning">Multi-device:</span> {multiNote}143 </Note>144 )}145146 {items.length === 0 ? (147 <EmptyState title={v.fitsOnly ? 'No model is estimated to fit' : 'No models to evaluate'} className="mt-6">148 {v.fitsOnly ? 'Try more memory, a lower-precision quantization, a shorter context or a smaller batch.' : 'Models without a sourced parameter count are not estimated.'}149 </EmptyState>150 ) : (151 <DataTable scroll compact className="mt-5">152 <thead>153 <tr>154 <Th>Model</Th>155 <Th num>Params</Th>156 <Th num>Est. memory</Th>157 <Th num>Headroom</Th>158 <Th>Fits</Th>159 <Th>Breakdown</Th>160 <Th>Artifacts</Th>161 <Th className="w-24" aria-label="Compare" />162 </tr>163 </thead>164 <tbody>165 {items.map((it, i) => {166 const openness = typeof it.model.attributes?.openness === 'string' ? it.model.attributes.openness : null;167 const head = num(it.fit.headroom_gb);168 return (169 <RowGroup key={`${it.model.id}-${i}`} it={it} openness={openness} head={head} />170 );171 })}172 {items.length === 0 && <EmptyRow cols={8} />}173 </tbody>174 </DataTable>175 )}176 <Note className="mt-3">177 Headroom = total device memory − reserve − estimate. {withArtifacts > 0 ? `${fmtInt(withArtifacts)} of ${fmtInt(items.length)} models have quantized artifacts recorded; ` : 'No quantized artifact is recorded for these models yet; '}178 artifact rows use the <span className="text-positive">observed</span> file size when a source publishes it, otherwise the estimate. Sorted by the API (fitting models first). Compare shortlisted models with the + buttons.179 </Note>180 <Methodology text={res.note} />181 </>182 )}183 </div>184 <CompareTrayBar />185 </TerminalLayout>186 );187}188189function RowGroup({ it, openness, head }: { it: RunLocallyItem; openness: string | null; head: number | null }) {190 return (191 <>192 <tr data-fit-row>193 <Td primary>194 <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5">195 <EntityLink e={it.model} />196 {openness && <OpennessBadge openness={openness} />}197 </span>198 {it.model.organization && <span className="block text-xs text-ink-3">{it.model.organization.name}</span>}199 {typeof it.model.attributes?.license === 'string' && <span className="block text-[11px] text-ink-3">{it.model.attributes.license as string}</span>}200 </Td>201 <Td num label="Params" className="tnum">{fmtParams(it.fit.parameter_count ?? it.model.attributes?.parameter_count)}</Td>202 <Td num label="Est. memory" className="tnum">203 {fmtGb(it.fit.estimated_memory_gb, 1)} <SourceTag source={it.fit.breakdown?.weights_source ?? 'estimated'} />204 </Td>205 <Td num label="Headroom" className={it.fit.fits ? 'tnum text-positive' : 'tnum text-danger'}>{head === null ? '—' : `${head >= 0 ? '+' : '−'}${fmtGb(Math.abs(head), 1)}`}</Td>206 <Td label="Fits" className={it.fit.fits ? 'font-medium text-positive' : 'text-ink-3'}>{it.fit.fits ? '✓ fits' : '✗ too large'}</Td>207 <Td label="Breakdown" wide>208 <FitBreakdownList fit={it.fit} />209 </Td>210 <Td label="Artifacts" className="text-xs text-ink-2">211 {num(it.artifact_count) ? `${fmtInt(it.artifact_count)} recorded` : <span className="text-ink-3">none recorded</span>}212 </Td>213 <Td className="text-right">214 <CompareButton e={it.model} size="sm" />215 </Td>216 </tr>217 {it.artifacts.slice(0, 4).map((a, j) => (218 <tr key={`${a.artifact.id}-${j}`} className="bg-surface-2/40" data-artifact-row>219 <Td primary className="!pl-6">220 <span className="text-xs text-ink-3">↳ </span>221 <EntityLink e={a.artifact} className="text-sm" />222 {a.quant_format && <Chip className="ml-2 uppercase">{a.quant_format}</Chip>}223 </Td>224 <Td num label="File size" className="tnum text-xs">225 {num(a.file_size_gb) === null ? '—' : fmtGb(a.file_size_gb, 1)} <SourceTag source={a.weights_source} />226 </Td>227 <Td num label="Est. memory" className="tnum text-xs">{fmtGb(a.fit.estimated_memory_gb, 1)}</Td>228 <Td num label="Headroom" className={a.fit.fits ? 'tnum text-xs text-positive' : 'tnum text-xs text-danger'}>{num(a.fit.headroom_gb) === null ? '—' : `${(num(a.fit.headroom_gb) as number) >= 0 ? '+' : '−'}${fmtGb(Math.abs(num(a.fit.headroom_gb) as number), 1)}`}</Td>229 <Td label="Fits" className={a.fit.fits ? 'text-xs font-medium text-positive' : 'text-xs text-ink-3'}>{a.fit.fits ? '✓ fits' : '✗ too large'}</Td>230 <Td label="Breakdown" wide>231 <FitBreakdownList fit={a.fit} />232 </Td>233 <Td label="Artifacts" className="mono text-[11px] text-ink-3">{a.fit.quantization}</Td>234 <Td />235 </tr>236 ))}237 {it.artifacts.length > 4 && (238 <tr className="bg-surface-2/40">239 <td colSpan={8} className="!py-1.5 pl-6 text-xs text-ink-3">240 +{it.artifacts.length - 4} more artifacts on the <EntityLink e={it.model} className="link">model page</EntityLink>.241 </td>242 </tr>243 )}244 </>245 );246}247