Lumbridge Bench
CI / verify (push) Successful in 24s
CI / deploy (push) Failing after 1m14s

This commit is contained in:
Karti Tripathi
2026-08-04 00:44:07 -07:00
commit 006feee0f7
65 changed files with 13516 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# Copy to .env.local. Never commit .env.local.
#
# Server-side destination for inert model suggestions. On web-host, Lumbridge
# Bench and the control plane share the host, so the private loopback endpoint
# avoids a public round trip.
LUMBRIDGE_CONTROL_PLANE_URL=http://127.0.0.1:8902
+171
View File
@@ -0,0 +1,171 @@
import fs from "node:fs";
import path from "node:path";
import { ImageResponse } from "next/og";
import { fmtParams, getRun, getRuns, limitOf } from "@/lib/results";
/**
* Shareable score card as a PNG.
*
* One image serves two jobs: the download button and the OpenGraph tag, so a
* pasted link unfurls as the card itself. Fonts are bundled rather than
* fetched at request time — satori needs real font buffers, and a network
* dependency inside image generation fails in exactly the situations where
* you want a share link to work.
*
* Note: satori supports flexbox only. Every container needs an explicit
* display value; CSS grid silently renders nothing.
*/
export const dynamic = "force-static";
export function generateStaticParams() {
return getRuns().map((r) => ({ id: r.run_id.slice(0, 8) }));
}
const FONT_DIR = path.join(process.cwd(), "assets", "fonts");
const font = (file: string) => fs.readFileSync(path.join(FONT_DIR, file));
const VOID = "#08090b";
const RULE = "#1e232b";
const INK = "#e8e6e1";
const DIM = "#79818d";
const DIMMER = "#4a515b";
const SIGNAL = "#f2b134";
const REFERENCE = "#5896ae";
function Stat({
label,
value,
unit,
color = INK,
hint,
}: {
label: string;
value: string;
unit?: string;
color?: string;
hint?: string;
}) {
return (
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
<div style={{ fontSize: 15, letterSpacing: 3, color: DIMMER, textTransform: "uppercase" }}>
{label}
</div>
<div style={{ display: "flex", alignItems: "baseline", marginTop: 12 }}>
<span style={{ fontFamily: "Instrument", fontSize: 62, color, lineHeight: 1 }}>{value}</span>
{unit && <span style={{ fontSize: 17, color: DIMMER, marginLeft: 7 }}>{unit}</span>}
</div>
{hint && <div style={{ fontSize: 14, color: DIMMER, marginTop: 8 }}>{hint}</div>}
</div>
);
}
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const run = getRun(id);
if (!run) return new Response("not found", { status: 404 });
const v = run.verdict;
const limit = limitOf(run);
const perf = run.perf?.points ?? [];
const single = perf.find((p) => p.concurrency === 1);
const peak = perf.length
? perf.reduce((a, b) => (b.output_tps_total > a.output_tps_total ? b : a))
: null;
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
background: VOID,
fontFamily: "Plex",
color: INK,
padding: 56,
}}
>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<div style={{ display: "flex", alignItems: "baseline" }}>
<span style={{ fontFamily: "Instrument", fontSize: 30 }}>bench</span>
<span style={{ fontFamily: "Instrument", fontSize: 30, color: DIMMER }}>.karti.ai</span>
</div>
<span style={{ fontSize: 15, letterSpacing: 3, color: DIMMER, textTransform: "uppercase" }}>
{run.timestamp.slice(0, 10)}
</span>
</div>
<div style={{ display: "flex", height: 1, background: RULE, marginTop: 24, marginBottom: 40 }} />
{/* title */}
<div style={{ display: "flex", flexDirection: "column" }}>
<span style={{ fontFamily: "Instrument", fontSize: 68, lineHeight: 1.05 }}>
{run.target.display_name}
</span>
<span style={{ fontSize: 18, color: DIM, marginTop: 16 }}>
{run.host.hardware} · {fmtParams(run.target)} ·{" "}
{run.target.quant?.toUpperCase()} · {run.target.serving.engine}
</span>
</div>
{/* stats */}
<div style={{ display: "flex", marginTop: "auto", gap: 24 }}>
<Stat
label="signal"
value={v.signal_score != null ? v.signal_score.toFixed(3) : "—"}
color={SIGNAL}
hint="our workloads"
/>
<Stat
label="reference"
value={v.reference_score != null ? v.reference_score.toFixed(3) : "—"}
color={REFERENCE}
hint={limit ? `capped ${limit}` : "calibration"}
/>
<Stat
label="single stream"
value={single ? single.output_tps_per_stream.toFixed(1) : "—"}
unit="tok/s"
hint={single?.ttft_p50_ms ? `ttft ${Math.round(single.ttft_p50_ms)}ms` : undefined}
/>
<Stat
label="peak"
value={peak ? peak.output_tps_total.toFixed(0) : "—"}
unit="tok/s"
hint={peak ? `at ×${peak.concurrency}` : undefined}
/>
</div>
{/* tick strip — a measurement scale across the full plate */}
<div style={{ display: "flex", marginTop: 40, justifyContent: "space-between", width: "100%" }}>
{Array.from({ length: 96 }).map((_, i) => (
<div
key={i}
style={{
display: "flex",
width: 1,
height: 8,
background: i % 6 === 0 ? SIGNAL : RULE,
}}
/>
))}
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{ name: "Instrument", data: font("InstrumentSerif-Regular.ttf"), style: "normal", weight: 400 },
{ name: "Plex", data: font("IBMPlexMono-Regular.ttf"), style: "normal", weight: 400 },
{ name: "Plex", data: font("IBMPlexMono-SemiBold.ttf"), style: "normal", weight: 600 },
],
},
);
}
+13
View File
@@ -0,0 +1,13 @@
import { getRuns } from "@/lib/results";
export const dynamic = "force-dynamic";
/** Liveness probe used by the deploy verify step. */
export async function GET() {
const runs = getRuns();
return Response.json({
ok: true,
runs: runs.length,
latest: runs[0]?.timestamp ?? null,
});
}
+73
View File
@@ -0,0 +1,73 @@
import { NextResponse } from "next/server";
import { parseHuggingFaceRef } from "@/lib/model-ref";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const controlPlaneUrl = (
process.env.LUMBRIDGE_CONTROL_PLANE_URL ?? "http://127.0.0.1:8902"
).replace(/\/+$/, "");
/**
* Record a Hugging Face model suggestion for Karti to review manually.
*
* This endpoint only forwards an inert review record to the Lumbridge control
* plane. Neither service downloads, imports, schedules, or executes a model.
*/
export async function POST(req: Request) {
let body: { model?: string; notes?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
}
const ref = parseHuggingFaceRef(body.model ?? "");
if (!ref) {
return NextResponse.json(
{ error: "Expected a Hugging Face model reference like `owner/model`." },
{ status: 400 },
);
}
try {
const response = await fetch(`${controlPlaneUrl}/api/model-suggestions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourceUrl: `https://huggingface.co/${ref}`,
reason: "Suggested via Lumbridge Bench for manual model review.",
notes: (body.notes ?? "").slice(0, 2000) || undefined,
}),
cache: "no-store",
signal: AbortSignal.timeout(5_000),
});
if (!response.ok) {
const result = await response.json().catch(() => null) as {
error?: string;
} | null;
return NextResponse.json(
{
error:
result?.error ??
"The Lumbridge review inbox could not accept that suggestion.",
},
{ status: response.status },
);
}
} catch {
return NextResponse.json(
{ error: "The Lumbridge review inbox is temporarily unavailable." },
{ status: 503 },
);
}
return NextResponse.json({
ok: true,
model: ref,
message:
"Suggestion received. Karti reviews every model manually. Nothing is " +
"downloaded or run automatically; approved results appear on the board later.",
});
}
+166
View File
@@ -0,0 +1,166 @@
@import "tailwindcss";
/* ---------------------------------------------------------------------------
Lumbridge Bench — model evidence lab.
This is a measurement tool, so it reads like a bench instrument rather than
a dashboard: near-black panel, hairline rules, tabular figures everywhere,
and a phosphor palette that carries meaning rather than decoration.
Colour is semantic and must stay that way:
signal (amber) — our private measurement. The real number.
reference (cyan) — public benchmark. Calibration only, never a ranking.
Anything that dilutes that distinction on screen undermines the whole point
of the two-tier design. See docs/DECISIONS.md#d5.
--------------------------------------------------------------------------- */
@theme {
--color-void: #08090b;
--color-panel: #0d0f13;
--color-panel-2: #121519;
--color-rule: #1e232b;
--color-rule-bright: #2c333d;
--color-ink: #e8e6e1;
--color-dim: #79818d;
--color-dimmer: #4a515b;
--color-signal: #f2b134;
--color-signal-dim: #6b4f18;
--color-reference: #5896ae;
--color-reference-dim: #23414d;
--color-pass: #52a86e;
--color-fail: #cb5a54;
--font-display: var(--font-instrument), ui-serif, Georgia, serif;
--font-mono: var(--font-plex-mono), ui-monospace, monospace;
}
:root {
color-scheme: dark;
}
html {
background: var(--color-void);
}
body {
color: var(--color-ink);
font-family: var(--font-mono);
font-feature-settings: "tnum" 1, "zero" 1;
-webkit-font-smoothing: antialiased;
}
/* Graph-paper substrate. Two grids at different scales, like a scope screen,
plus a vignette so the panel edges fall away rather than ending abruptly. */
.substrate {
position: fixed;
inset: 0;
z-index: -2;
background-color: var(--color-void);
background-image:
linear-gradient(rgba(120, 140, 160, 0.028) 1px, transparent 1px),
linear-gradient(90deg, rgba(120, 140, 160, 0.028) 1px, transparent 1px),
linear-gradient(rgba(120, 140, 160, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(120, 140, 160, 0.055) 1px, transparent 1px);
background-size: 22px 22px, 22px 22px, 110px 110px, 110px 110px;
}
.substrate::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
ellipse 120% 80% at 50% 0%,
transparent 20%,
rgba(8, 9, 11, 0.75) 75%,
var(--color-void) 100%
);
}
/* Film grain. Keeps large dark fields from banding and gives the panel a
physical, slightly analogue quality. */
.grain {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: 0.16;
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}
/* Instrument label: the small stamped caps used on panel headings. */
.label {
font-size: 0.6875rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-dimmer);
}
/* Numbers that should read as a live readout. */
.readout {
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
/* Hairline panel. Border colour is deliberately close to the background —
structure should be felt more than seen. */
.panel {
background: linear-gradient(180deg, var(--color-panel) 0%, var(--color-void) 100%);
border: 1px solid var(--color-rule);
}
/* Tick strip used as a section divider — evokes a measurement scale. */
.ticks {
height: 6px;
background-image: repeating-linear-gradient(
90deg,
var(--color-rule-bright) 0 1px,
transparent 1px 9px
);
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Staggered page-load reveal. One orchestrated entrance rather than scattered
micro-interactions. */
.rise {
animation: rise 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes trace {
from {
stroke-dashoffset: var(--trace-len);
}
to {
stroke-dashoffset: 0;
}
}
/* The throughput curve draws itself like a scope sweep. */
.trace {
stroke-dasharray: var(--trace-len);
animation: trace 1.1s cubic-bezier(0.4, 0, 0.2, 1) 0.25s both;
}
@media (prefers-reduced-motion: reduce) {
.rise,
.trace {
animation: none;
}
}
::selection {
background: var(--color-signal);
color: var(--color-void);
}
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#08090b"/>
<!-- a rising throughput trace: the mark is the measurement -->
<path d="M4 24 L11 20 L18 11 L28 7" fill="none" stroke="#f2b134" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 28 h24" stroke="#2c333d" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

+88
View File
@@ -0,0 +1,88 @@
import type { Metadata } from "next";
import { IBM_Plex_Mono, Instrument_Serif } from "next/font/google";
import Link from "next/link";
import "./globals.css";
const instrument = Instrument_Serif({
weight: "400",
subsets: ["latin"],
variable: "--font-instrument",
display: "swap",
});
const plexMono = IBM_Plex_Mono({
weight: ["400", "500", "600"],
subsets: ["latin"],
variable: "--font-plex-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "Lumbridge Bench",
description:
"Evidence for models on Lumbridge Compute: quality, serving performance, and exact hardware provenance.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${instrument.variable} ${plexMono.variable}`}>
<body className="min-h-screen antialiased">
<div className="substrate" />
<div className="grain" />
<header className="border-b border-rule">
<div className="mx-auto flex max-w-6xl items-baseline justify-between px-6 py-5">
<Link href="/" className="group flex items-baseline gap-3">
<span className="font-display text-2xl leading-none tracking-tight">
Lumbridge
<span className="text-dimmer"> Bench</span>
</span>
<span className="label hidden transition-colors group-hover:text-signal sm:inline">
evidence lab
</span>
</Link>
<nav className="flex items-center gap-6 text-xs">
<Link
href="/"
className="text-dim transition-colors hover:text-ink"
>
board
</Link>
<Link
href="/method"
className="text-dim transition-colors hover:text-ink"
>
method
</Link>
<Link
href="/submit"
className="border border-rule-bright px-3 py-1.5 text-ink transition-colors hover:border-signal hover:text-signal"
>
suggest a model
</Link>
</nav>
</div>
</header>
{children}
<footer className="mt-24 border-t border-rule">
<div className="mx-auto max-w-6xl px-6 py-8">
<div className="ticks mb-6 opacity-40" />
<div className="flex flex-col gap-3 text-xs text-dimmer sm:flex-row sm:justify-between">
<p>
Measured on our own hardware. Reference scores are public
benchmarks and are calibration only.
</p>
<p className="font-display text-sm text-dim">Lumbridge Bench</p>
</div>
</div>
</footer>
</body>
</html>
);
}
+142
View File
@@ -0,0 +1,142 @@
export const dynamic = "force-static";
export const metadata = {
title: "Method — Lumbridge Bench",
description:
"How these numbers are produced, and what they do and do not mean.",
};
function Section({
n,
title,
children,
}: {
n: string;
title: string;
children: React.ReactNode;
}) {
return (
<section className="border-t border-rule py-10">
<div className="grid gap-6 md:grid-cols-[auto_1fr] md:gap-12">
<div className="label pt-1 md:w-16">{n}</div>
<div>
<h2 className="font-display mb-4 text-2xl">{title}</h2>
<div className="max-w-2xl space-y-4 text-xs leading-relaxed text-dim">
{children}
</div>
</div>
</div>
</section>
);
}
export default function MethodPage() {
return (
<main className="mx-auto max-w-5xl px-6 py-14">
<div className="rise mb-10 max-w-2xl">
<h1 className="font-display text-4xl leading-tight sm:text-5xl">
Method
</h1>
<p className="mt-5 text-sm leading-relaxed text-dim">
What these numbers are, how they are produced, and the part most
leaderboards skip what they cannot tell you.
</p>
</div>
<Section n="01" title="A target is not a model">
<p>
Every card measures a{" "}
<span className="text-ink">
model × host × quantization × serving config × checkpoint
</span>
, not a model name. On unified-memory hardware the serving flags move
throughput more than swapping the model does disabling CUDA graphs,
or capping memory utilization at 55%, changes the answer by more than
the difference between two model families.
</p>
<p>
So the flags are recorded from the live server on every run and shown
on the card. Two rows are only comparable if their configurations
match.
</p>
</Section>
<Section n="02" title="Two tiers, and only one is a ranking">
<p>
<span className="text-signal">Signal</span> tasks are private, built
from work we actually do. They are the measurement.
</p>
<p>
<span className="text-reference">Reference</span> tasks are public
benchmarks run unmodified. They exist to calibrate the harness, not to
rank models: if our number lands far from the published value, our
harness is broken. Public benchmarks have been in training data for
years, and a high reference score is not evidence of much.
</p>
<p>
A bench made only of private tasks would be unfalsifiable no reader
could distinguish a bad model from a broken harness. That is what the
reference tier is for.
</p>
</Section>
<Section n="03" title="Why the private set stays private">
<p>
The usual reason given is confidentiality. The real reason is
contamination: a published test set gets scraped, trained on, and
stops measuring capability. That is how MMLU, GSM8K and HumanEval
stopped being informative.
</p>
<p>
Samples carry embedded canary strings, so if a future model reproduces
them we can demonstrate contamination rather than suspect it.
</p>
</Section>
<Section n="04" title="Performance is measured, not quoted">
<p>
Each run drives real concurrent load at the serving endpoint and
records time-to-first-token, inter-token latency, per-stream decode
rate, and aggregate throughput at several concurrency levels.
</p>
<p>
Two details do most of the work. Every request carries a unique prefix
so <span className="text-ink">prefix caching</span> cannot make
prefill free and inflate the result. And output length is pinned, so
concurrency levels finish at identical token counts and remain
comparable. Without both, the numbers look considerably better and
mean nothing.
</p>
<p>
Single-stream and aggregate figures are both reported because they
disagree. On our hardware, throughput rises roughly 12× from one
stream to thirty-two while per-stream decode falls to a third batch
serving and interactive use want opposite configurations.
</p>
</Section>
<Section n="05" title="What a capped run means">
<p>
Long reference datasets against a slow local model take hours, so some
runs are capped at a sample limit. When that happens the cap is
recorded in the result and displayed on the card. A capped score is
not comparable to a full-dataset score and is never presented as one.
</p>
</Section>
<Section n="06" title="Known limits">
<p>
Single hardware sample per host no variance across identical
machines. Perf runs are single-shot rather than averaged over repeats,
so treat small differences between runs as noise; the run-to-run
spread on aggregate throughput is meaningful.
</p>
<p>
Rubric-graded samples depend on a judge model, which drifts as that
model changes. Deterministic scorers are preferred wherever the
property can be checked mechanically.
</p>
</Section>
</main>
);
}
+280
View File
@@ -0,0 +1,280 @@
import Link from "next/link";
import { ScopeTrace } from "@/components/ScopeTrace";
import {
fmtParams,
getRuns,
latestPerTarget,
limitOf,
type Run,
} from "@/lib/results";
export const dynamic = "force-static";
function Metric({
label,
value,
unit,
tone = "ink",
hint,
}: {
label: string;
value: string;
unit?: string;
tone?: "ink" | "signal" | "reference" | "dim";
hint?: string;
}) {
const toneClass = {
ink: "text-ink",
signal: "text-signal",
reference: "text-reference",
dim: "text-dim",
}[tone];
return (
<div className="min-w-0">
<div className="label mb-2">{label}</div>
<div
className={`readout font-display text-4xl leading-none ${toneClass}`}
>
{value}
{unit && (
<span className="ml-1 font-mono text-sm text-dimmer">{unit}</span>
)}
</div>
{hint && (
<div className="mt-2 text-[11px] leading-snug text-dimmer">{hint}</div>
)}
</div>
);
}
function PrimaryCard({ run }: { run: Run }) {
const v = run.verdict;
const limit = limitOf(run);
const perf = run.perf?.points ?? [];
const single = perf.find((p) => p.concurrency === 1);
const peak = perf.length
? perf.reduce((a, b) => (b.output_tps_total > a.output_tps_total ? b : a))
: null;
return (
<section className="rise panel relative overflow-hidden">
<div className="absolute right-0 top-0 border-b border-l border-rule px-3 py-1.5">
<span className="label text-signal">in production</span>
</div>
<div className="border-b border-rule p-6 sm:p-8">
<div className="label mb-3">primary target</div>
<h2 className="font-display text-3xl leading-tight sm:text-4xl">
{run.target.display_name}
</h2>
<p className="mt-3 max-w-2xl text-xs leading-relaxed text-dim">
{run.host.hardware} · {fmtParams(run.target)} ·{" "}
{run.target.quant?.toUpperCase()} · {run.target.serving.engine} · ctx{" "}
{run.target.serving.max_model_len?.toLocaleString()}
</p>
</div>
<div className="grid gap-8 border-b border-rule p-6 sm:grid-cols-2 sm:p-8 lg:grid-cols-4">
<Metric
label="signal score"
value={v.signal_score != null ? v.signal_score.toFixed(3) : "—"}
tone="signal"
hint={
v.signal_score == null
? "no private tasks authored yet"
: "our own workloads"
}
/>
<Metric
label="reference"
value={v.reference_score != null ? v.reference_score.toFixed(3) : "—"}
tone="reference"
hint={
limit
? `capped at ${limit} samples · calibration only`
: "calibration only"
}
/>
<Metric
label="single stream"
value={single ? single.output_tps_per_stream.toFixed(1) : "—"}
unit="tok/s"
hint={
single
? `TTFT ${Math.round(single.ttft_p50_ms ?? 0)}ms · what one user feels`
: undefined
}
/>
<Metric
label="peak throughput"
value={peak ? peak.output_tps_total.toFixed(0) : "—"}
unit="tok/s"
hint={
peak
? `at concurrency ${peak.concurrency} · what the box serves`
: undefined
}
/>
</div>
{perf.length > 1 && (
<div className="p-6 sm:p-8">
<ScopeTrace points={perf} />
<div className="mt-4 flex flex-wrap items-baseline justify-between gap-3">
<p className="max-w-xl text-[11px] leading-relaxed text-dimmer">
Throughput scales{" "}
{peak && single
? `${(peak.output_tps_total / single.output_tps_per_stream).toFixed(1)}×`
: "—"}{" "}
from one stream to {peak?.concurrency ?? "—"}, but per-stream
decode falls to {peak?.output_tps_per_stream.toFixed(1) ?? "—"}{" "}
tok/s. Batch work and interactive work want opposite settings on
this box.
</p>
<Link
href={`/run/${run.run_id.slice(0, 8)}`}
className="shrink-0 border border-rule-bright px-3 py-1.5 text-xs transition-colors hover:border-signal hover:text-signal"
>
full score card
</Link>
</div>
</div>
)}
</section>
);
}
export default function Home() {
const runs = getRuns();
const board = latestPerTarget(runs);
const primary = board.find((r) => r.target.tier === "production") ?? board[0];
const lastMeasured = runs[0]?.timestamp?.slice(0, 10);
return (
<main className="mx-auto max-w-6xl px-6 py-14">
<div className="rise mb-14 max-w-3xl">
<h1 className="font-display text-5xl leading-[1.05] sm:text-6xl">
Does this model earn
<br />
its place on our hardware?
</h1>
<p className="mt-6 max-w-xl text-sm leading-relaxed text-dim">
Lumbridge Bench is the evidence layer for Lumbridge Compute. Two
numbers decide a self-hosting call: whether a model is good enough at
the work we actually do, and whether it is fast enough on the box we
would run it on. Most leaderboards report only the first. Every card
here reports both, measured together, on the same machine.
</p>
<div className="mt-8 flex flex-wrap items-center gap-x-8 gap-y-3 text-xs text-dimmer">
<span>
<span className="text-ink">{board.length}</span> target
{board.length === 1 ? "" : "s"}
</span>
<span>
<span className="text-ink">{runs.length}</span> run
{runs.length === 1 ? "" : "s"}
</span>
{lastMeasured && (
<span>
last measured <span className="text-ink">{lastMeasured}</span>
</span>
)}
</div>
</div>
{primary && <PrimaryCard run={primary} />}
<div className="ticks mt-16 mb-8 opacity-40" />
<section>
<div className="mb-6 flex items-baseline justify-between">
<h2 className="font-display text-2xl">The board</h2>
<span className="label">latest run per target</span>
</div>
<div className="-mx-6 overflow-x-auto px-6">
<table className="w-full min-w-[720px] border-collapse text-xs">
<thead>
<tr className="border-b border-rule-bright text-left">
<th className="label py-3 pr-4 font-normal">target</th>
<th className="label py-3 pr-4 font-normal">host</th>
<th className="label py-3 pr-4 text-right font-normal">
signal
</th>
<th className="label py-3 pr-4 text-right font-normal">
reference
</th>
<th className="label py-3 pr-4 text-right font-normal">
tok/s ×1
</th>
<th className="label py-3 pr-4 text-right font-normal">
peak tok/s
</th>
<th className="label py-3 text-right font-normal">measured</th>
</tr>
</thead>
<tbody>
{board.map((run) => {
const v = run.verdict;
return (
<tr
key={run.run_id}
className="group border-b border-rule transition-colors hover:bg-panel-2"
>
<td className="py-4 pr-4">
<Link
href={`/run/${run.run_id.slice(0, 8)}`}
className="block"
>
<span className="text-ink transition-colors group-hover:text-signal">
{run.target.display_name}
</span>
<span className="mt-1 block text-[11px] text-dimmer">
{fmtParams(run.target)} ·{" "}
{run.target.quant?.toUpperCase()}
</span>
</Link>
</td>
<td className="py-4 pr-4 text-dim">{run.host.id}</td>
<td className="readout py-4 pr-4 text-right text-signal">
{v.signal_score != null ? v.signal_score.toFixed(3) : "—"}
</td>
<td className="readout py-4 pr-4 text-right text-reference">
{v.reference_score != null
? v.reference_score.toFixed(3)
: "—"}
</td>
<td className="readout py-4 pr-4 text-right">
{v.single_stream_tps?.toFixed(1) ?? "—"}
</td>
<td className="readout py-4 pr-4 text-right">
{v.peak_throughput_tps?.toFixed(0) ?? "—"}
</td>
<td className="py-4 text-right text-dimmer">
{run.timestamp.slice(0, 10)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="mt-8 grid gap-4 text-[11px] leading-relaxed text-dimmer sm:grid-cols-2">
<p className="border-l border-signal-dim pl-4">
<span className="text-signal">Signal</span> scores come from private
tasks built from our own workloads. They are the measurement. The
set is not published a public test set gets scraped into the next
training run and stops measuring anything.
</p>
<p className="border-l border-reference-dim pl-4">
<span className="text-reference">Reference</span> scores come from
public benchmarks, unmodified. They are calibration, not a ranking:
if one lands far from its published value, our harness is wrong, not
the model.
</p>
</div>
</section>
</main>
);
}
+219
View File
@@ -0,0 +1,219 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { ScopeTrace } from "@/components/ScopeTrace";
import { fmtParams, getRun, getRuns, limitOf, type QualityResult } from "@/lib/results";
export const dynamic = "force-static";
export function generateStaticParams() {
return getRuns().map((r) => ({ id: r.run_id.slice(0, 8) }));
}
const TIER_TONE = {
signal: { text: "text-signal", border: "border-signal-dim", chip: "bg-signal-dim/30" },
reference: { text: "text-reference", border: "border-reference-dim", chip: "bg-reference-dim/30" },
example: { text: "text-dim", border: "border-rule", chip: "bg-rule" },
} as const;
/**
* Per-sample outcome grid.
*
* This is the reason the result schema stores samples and not just aggregates:
* an aggregate says a checkpoint got worse, this says which samples broke.
*/
function SampleGrid({ q }: { q: QualityResult }) {
if (!q.samples.length) return null;
return (
<div className="mt-5">
<div className="label mb-3">
per-sample outcomes · {q.samples.filter((s) => s.passed).length}/{q.samples.length} passed
</div>
<div className="flex flex-wrap gap-[3px]">
{q.samples.map((s) => (
<span
key={s.sample_id}
title={`${s.sample_id}${s.passed ? "pass" : "fail"}`}
className={`h-3 w-3 ${s.passed ? "bg-pass/70" : "bg-fail/80"}`}
/>
))}
</div>
</div>
);
}
export default async function RunPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const run = getRun(id);
if (!run) notFound();
const limit = limitOf(run);
const perf = run.perf?.points ?? [];
const flags = Object.entries(run.target.serving.flags ?? {});
return (
<main className="mx-auto max-w-6xl px-6 py-14">
<Link href="/" className="label transition-colors hover:text-signal">
board
</Link>
<header className="rise mt-6 mb-10">
<h1 className="font-display text-4xl leading-tight sm:text-5xl">
{run.target.display_name}
</h1>
<p className="mt-3 font-mono text-xs text-dim">{run.target_id}</p>
<div className="mt-6 flex flex-wrap gap-x-6 gap-y-2 text-[11px] text-dimmer">
<span>{run.host.hardware}</span>
<span>{fmtParams(run.target)}</span>
<span>{run.target.quant?.toUpperCase()}</span>
<span>ctx {run.target.serving.max_model_len?.toLocaleString()}</span>
<span>measured {run.timestamp.slice(0, 16).replace("T", " ")}Z</span>
</div>
</header>
{limit && (
<div className="mb-10 border-l-2 border-signal bg-signal-dim/10 px-4 py-3 text-xs text-dim">
<span className="text-signal">Capped run.</span> Reference tasks were
limited to {limit} samples. Not comparable to a full-dataset score.
</div>
)}
<div className="grid gap-10 lg:grid-cols-[1.15fr_1fr]">
<section>
<h2 className="font-display mb-5 text-2xl">Quality</h2>
<div className="space-y-4">
{run.quality.length === 0 && (
<p className="text-xs text-dimmer">
Perf-only run no quality tasks were executed.
</p>
)}
{run.quality.map((q) => {
const tone = TIER_TONE[q.tier] ?? TIER_TONE.example;
const acc = q.metrics?.accuracy;
return (
<div key={q.task} className={`panel border-l-2 ${tone.border} p-5`}>
<div className="flex items-baseline justify-between gap-4">
<div>
<span className="text-sm text-ink">{q.task}</span>
<span className={`ml-3 px-2 py-0.5 text-[10px] uppercase tracking-widest ${tone.chip} ${tone.text}`}>
{q.tier}
</span>
</div>
<span className={`readout font-display text-3xl ${tone.text}`}>
{q.error ? "ERR" : acc != null ? acc.toFixed(3) : "—"}
</span>
</div>
{q.error ? (
<p className="mt-3 font-mono text-[11px] leading-relaxed text-fail">
{q.error.slice(0, 240)}
</p>
) : (
<>
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-1 text-[11px] text-dimmer">
<span>{q.n_samples} samples</span>
{q.duration_s && <span>{Math.round(q.duration_s)}s</span>}
{Object.entries(q.metrics)
.filter(([k]) => !k.startsWith("_") && k !== "accuracy")
.slice(0, 4)
.map(([k, val]) => (
<span key={k}>
{k} <span className="text-dim">{val.toFixed(3)}</span>
</span>
))}
</div>
<SampleGrid q={q} />
</>
)}
</div>
);
})}
</div>
</section>
<section>
<h2 className="font-display mb-5 text-2xl">Serving performance</h2>
{perf.length > 1 && (
<div className="panel mb-5 p-5">
<ScopeTrace points={perf} />
</div>
)}
<div className="overflow-x-auto">
<table className="w-full border-collapse text-[11px]">
<thead>
<tr className="border-b border-rule-bright text-left">
<th className="label py-2 pr-3 font-normal">conc</th>
<th className="label py-2 pr-3 text-right font-normal">tok/s all</th>
<th className="label py-2 pr-3 text-right font-normal">/stream</th>
<th className="label py-2 pr-3 text-right font-normal">ttft p50</th>
<th className="label py-2 pr-3 text-right font-normal">ttft p95</th>
<th className="label py-2 text-right font-normal">tpot</th>
</tr>
</thead>
<tbody>
{perf.map((p) => (
<tr key={p.concurrency} className="border-b border-rule">
<td className="readout py-2.5 pr-3">{p.concurrency}</td>
<td className="readout py-2.5 pr-3 text-right text-signal">
{p.output_tps_total.toFixed(1)}
</td>
<td className="readout py-2.5 pr-3 text-right">
{p.output_tps_per_stream.toFixed(1)}
</td>
<td className="readout py-2.5 pr-3 text-right text-dim">
{p.ttft_p50_ms ? `${Math.round(p.ttft_p50_ms)}ms` : "—"}
</td>
<td className="readout py-2.5 pr-3 text-right text-dim">
{p.ttft_p95_ms ? `${Math.round(p.ttft_p95_ms)}ms` : "—"}
</td>
<td className="readout py-2.5 text-right text-dim">
{p.tpot_p50_ms ? `${p.tpot_p50_ms.toFixed(1)}ms` : "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
{flags.length > 0 && (
<div className="mt-8">
<div className="label mb-3">serving configuration</div>
<p className="mb-4 text-[11px] leading-relaxed text-dimmer">
Results are only comparable between targets with identical flags.
These are recorded from the live server, not from memory.
</p>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 font-mono text-[11px]">
{flags.map(([k, v]) => (
<div key={k} className="contents">
<dt className="text-dimmer">{k}</dt>
<dd className="text-dim">{String(v)}</dd>
</div>
))}
</dl>
</div>
)}
</section>
</div>
{run.target.notes && (
<section className="mt-14 border-t border-rule pt-8">
<div className="label mb-3">notes</div>
<p className="max-w-3xl text-xs leading-relaxed text-dim">{run.target.notes}</p>
</section>
)}
<section className="mt-10 flex flex-wrap items-center gap-4 border-t border-rule pt-8">
<a
href={`/api/card/${run.run_id.slice(0, 8)}`}
className="border border-rule-bright px-4 py-2 text-xs transition-colors hover:border-signal hover:text-signal"
>
download score card
</a>
<span className="text-[11px] text-dimmer">
PNG, sized for sharing. Same image the link preview uses.
</span>
</section>
</main>
);
}
+155
View File
@@ -0,0 +1,155 @@
"use client";
import { useState } from "react";
type State =
| { kind: "idle" }
| { kind: "sending" }
| { kind: "ok"; message: string; model: string }
| { kind: "error"; message: string };
export default function SubmitPage() {
const [model, setModel] = useState("");
const [notes, setNotes] = useState("");
const [state, setState] = useState<State>({ kind: "idle" });
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setState({ kind: "sending" });
try {
const res = await fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model, notes }),
});
const data = await res.json();
if (!res.ok) {
setState({
kind: "error",
message: data.error ?? "Something went wrong.",
});
return;
}
setState({ kind: "ok", message: data.message, model: data.model });
setModel("");
setNotes("");
} catch {
setState({ kind: "error", message: "Network error." });
}
}
const busy = state.kind === "sending";
return (
<main className="mx-auto max-w-3xl px-6 py-14">
<div className="rise">
<h1 className="font-display text-4xl leading-tight sm:text-5xl">
Suggest a model
</h1>
<p className="mt-5 max-w-xl text-sm leading-relaxed text-dim">
Share a Hugging Face model you think belongs in the lab. Karti reviews
every suggestion, inspects the files and license, and decides what to
run. Nothing is downloaded or executed automatically. No account
needed.
</p>
</div>
<form
onSubmit={onSubmit}
className="rise panel mt-10 p-6 sm:p-8"
style={{ animationDelay: "80ms" }}
>
<label className="label block" htmlFor="model">
huggingface model
</label>
<input
id="model"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Qwen/Qwen3-8B"
required
spellCheck={false}
className="mt-3 w-full border border-rule bg-void px-4 py-3 font-mono text-sm text-ink outline-none transition-colors placeholder:text-dimmer focus:border-signal"
/>
<p className="mt-2 text-[11px] text-dimmer">
`owner/model`, or paste the full huggingface.co URL.
</p>
<label className="label mt-8 block" htmlFor="notes">
anything we should know{" "}
<span className="text-dimmer">(optional)</span>
</label>
<textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
placeholder="Recommended quant, chat template quirks, what it is meant to be good at…"
className="mt-3 w-full resize-y border border-rule bg-void px-4 py-3 font-mono text-xs text-ink outline-none transition-colors placeholder:text-dimmer focus:border-signal"
/>
<button
type="submit"
disabled={busy || !model.trim()}
className="mt-8 w-full border border-rule-bright px-4 py-3 text-xs uppercase tracking-widest transition-colors hover:border-signal hover:text-signal disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-rule-bright disabled:hover:text-ink"
>
{busy ? "sending suggestion…" : "send suggestion"}
</button>
{state.kind === "error" && (
<p className="mt-5 border-l-2 border-fail px-4 py-2 text-xs leading-relaxed text-fail">
{state.message}
</p>
)}
{state.kind === "ok" && (
<div className="mt-5 border-l-2 border-pass px-4 py-2">
<p className="text-xs text-pass">{state.model} suggested.</p>
<p className="mt-1 text-[11px] leading-relaxed text-dim">
{state.message}
</p>
</div>
)}
</form>
<section className="mt-12 grid gap-8 sm:grid-cols-2">
<div>
<h2 className="label mb-4">what helps review</h2>
<ul className="space-y-3 text-[11px] leading-relaxed text-dim">
<li className="border-l border-rule pl-4">
<span className="text-ink">safetensors weights.</span> We do not
load `.bin` checkpoints they are pickles, and loading one is
remote code execution on our hardware.
</li>
<li className="border-l border-rule pl-4">
<span className="text-ink">A clear source.</span> Public or gated
is fine to suggest; access is reviewed manually.
</li>
<li className="border-l border-rule pl-4">
<span className="text-ink">Useful context.</span> Tell us the
recommended quant, license, and what the model is meant to do.
</li>
</ul>
</div>
<div>
<h2 className="label mb-4">what happens next</h2>
<ul className="space-y-3 text-[11px] leading-relaxed text-dim">
<li className="border-l border-rule pl-4">
If Karti approves and schedules it, Lumbridge Bench records
quality and serving performance on named hardware with the exact
serving flags.
</li>
<li className="border-l border-rule pl-4">
Private eval samples and internal review notes never become
public. Selected aggregate score cards can be published after
review.
</li>
<li className="border-l border-rule pl-4">
A suggestion is not a promise to run a model. There is no
automatic queue or public-code worker.
</li>
</ul>
</div>
</section>
</main>
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+134
View File
@@ -0,0 +1,134 @@
import type { PerfPoint } from "@/lib/results";
/**
* Aggregate throughput plotted against concurrency, drawn as a scope sweep.
*
* The x axis is logarithmic because concurrency is sampled in octaves
* (1, 8, 32). On a linear axis the low-concurrency points — the ones that
* describe what a single user actually feels — collapse against the origin
* and the most important part of the curve becomes unreadable.
*/
export function ScopeTrace({
points,
width = 520,
height = 150,
showAxis = true,
}: {
points: PerfPoint[];
width?: number;
height?: number;
showAxis?: boolean;
}) {
if (points.length < 2) return null;
const pad = { top: 14, right: 14, bottom: showAxis ? 22 : 10, left: showAxis ? 46 : 10 };
const w = width - pad.left - pad.right;
const h = height - pad.top - pad.bottom;
const xs = points.map((p) => Math.log2(Math.max(1, p.concurrency)));
const ys = points.map((p) => p.output_tps_total);
const xMax = Math.max(...xs) || 1;
const yMax = Math.max(...ys) * 1.15;
const px = (i: number) => pad.left + (xs[i] / xMax) * w;
const py = (v: number) => pad.top + h - (v / yMax) * h;
const path = points.map((p, i) => `${i === 0 ? "M" : "L"}${px(i)},${py(p.output_tps_total)}`).join(" ");
const area = `${path} L${px(points.length - 1)},${pad.top + h} L${px(0)},${pad.top + h} Z`;
// Rough path length so the draw-on animation has a dash offset to work with.
const traceLen = Math.round(w * 1.6);
const gridLines = [0, 0.25, 0.5, 0.75, 1];
return (
<svg
viewBox={`0 0 ${width} ${height}`}
className="w-full"
role="img"
aria-label="Aggregate throughput against concurrency"
>
<defs>
<linearGradient id="trace-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-signal)" stopOpacity="0.22" />
<stop offset="100%" stopColor="var(--color-signal)" stopOpacity="0" />
</linearGradient>
</defs>
{gridLines.map((g) => (
<line
key={g}
x1={pad.left}
x2={pad.left + w}
y1={pad.top + h - g * h}
y2={pad.top + h - g * h}
stroke="var(--color-rule)"
strokeWidth="1"
/>
))}
{showAxis &&
gridLines
.filter((g) => g > 0)
.map((g) => (
<text
key={`l${g}`}
x={pad.left - 8}
y={pad.top + h - g * h + 3}
textAnchor="end"
className="fill-[var(--color-dimmer)]"
style={{ fontSize: 9, fontFamily: "var(--font-mono)" }}
>
{Math.round(yMax * g)}
</text>
))}
<path d={area} fill="url(#trace-fill)" />
<path
d={path}
fill="none"
stroke="var(--color-signal)"
strokeWidth="1.75"
strokeLinejoin="round"
strokeLinecap="round"
className="trace"
style={{ ["--trace-len" as string]: traceLen }}
/>
{points.map((p, i) => (
<g key={p.concurrency}>
<circle
cx={px(i)}
cy={py(p.output_tps_total)}
r="3"
fill="var(--color-void)"
stroke="var(--color-signal)"
strokeWidth="1.5"
/>
{showAxis && (
<text
x={px(i)}
y={height - 6}
textAnchor="middle"
className="fill-[var(--color-dimmer)]"
style={{ fontSize: 9, fontFamily: "var(--font-mono)" }}
>
{p.concurrency}
</text>
)}
</g>
))}
{showAxis && (
<text
x={pad.left}
y={11}
className="fill-[var(--color-dimmer)]"
style={{ fontSize: 9, letterSpacing: "0.14em", fontFamily: "var(--font-mono)" }}
>
TOK/S AGGREGATE / CONCURRENCY
</text>
)}
</svg>
);
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Normalize a public Hugging Face reference before it becomes an inert
* Lumbridge model-suggestion record.
*/
export function parseHuggingFaceRef(input: string): string | null {
const trimmed = input.trim().replace(/\/+$/, "");
const url = trimmed
.replace(/^https?:\/\/(www\.)?huggingface\.co\//i, "")
.replace(/^hf:\/\//i, "");
// owner/model — letters, digits, dot, dash, underscore.
return /^[\w.-]+\/[\w.-]+$/.test(url) ? url : null;
}
+122
View File
@@ -0,0 +1,122 @@
import fs from "node:fs";
import path from "node:path";
/**
* The leaderboard is a view over committed JSON, not a database.
*
* results/*.json is the record of every run and it lives in git, so the site
* has no state of its own and cannot drift from what was actually measured.
* Supabase enters only for submissions and auth, which are genuinely mutable.
*/
const RESULTS_DIR = path.join(process.cwd(), "..", "results");
export type SampleOutcome = {
sample_id: string;
score: number;
passed: boolean;
excerpt: string | null;
metadata: Record<string, unknown>;
};
export type QualityResult = {
task: string;
tier: "reference" | "signal" | "example";
dataset_version: string | null;
n_samples: number;
metrics: Record<string, number>;
samples: SampleOutcome[];
duration_s: number | null;
error: string | null;
};
export type PerfPoint = {
concurrency: number;
input_tokens: number;
output_tokens: number;
completed: number;
failed: number;
output_tps_total: number;
output_tps_per_stream: number;
ttft_p50_ms: number | null;
ttft_p95_ms: number | null;
tpot_p50_ms: number | null;
prefill_tps: number | null;
};
export type Run = {
run_id: string;
timestamp: string;
target_id: string;
target: {
display_name: string;
family: string | null;
params_b: number | null;
active_params_b: number | null;
quant: string | null;
checkpoint: string | null;
tier: string;
host: string;
slug: string;
notes: string | null;
serving: {
engine: string;
model_name: string;
max_model_len: number | null;
flags: Record<string, unknown>;
};
};
host: { id: string; hardware: string | null; memory_gb: number | null };
quality: QualityResult[];
perf: { engine: string; points: PerfPoint[] } | null;
verdict: {
signal_score: number | null;
reference_score: number | null;
single_stream_tps?: number;
interactive_viable?: boolean;
peak_throughput_tps?: number;
peak_throughput_concurrency?: number;
};
schema_version: number;
};
export function getRuns(): Run[] {
if (!fs.existsSync(RESULTS_DIR)) return [];
return fs
.readdirSync(RESULTS_DIR)
.filter((f) => f.endsWith(".json"))
.map((f) => JSON.parse(fs.readFileSync(path.join(RESULTS_DIR, f), "utf8")) as Run)
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
}
export function getRun(id: string): Run | undefined {
return getRuns().find((r) => r.run_id.startsWith(id));
}
/**
* Latest run per target — what the leaderboard ranks.
*
* Ranking every run would let a target dominate the board simply by being
* measured more often.
*/
export function latestPerTarget(runs: Run[]): Run[] {
const seen = new Map<string, Run>();
for (const run of runs) {
if (!seen.has(run.target_id)) seen.set(run.target_id, run);
}
return [...seen.values()];
}
/** Was any task in this run capped with --limit? Must never be hidden. */
export function limitOf(run: Run): number | null {
for (const q of run.quality) {
if (q.metrics?._limit) return q.metrics._limit;
}
return null;
}
export function fmtParams(t: Run["target"]): string {
if (!t.params_b) return "—";
const base = `${t.params_b}B`;
return t.active_params_b ? `${base}·${t.active_params_b}B active` : base;
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+17
View File
@@ -0,0 +1,17 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
const siteDir = path.dirname(fileURLToPath(import.meta.url));
/** @type {import('next').NextConfig} */
const nextConfig = {
outputFileTracingRoot: path.join(siteDir, ".."),
outputFileTracingIncludes: {
// Score cards are read from the repo's results/ directory at build time.
// There is no database behind the leaderboard: the git history IS the
// record, so the site is a view over committed JSON.
"/**": ["../results/**"],
},
};
export default nextConfig;
+1676
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "lumbridge-bench-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 8909",
"build": "next build",
"start": "next start -p 8909",
"lint": "tsc --noEmit"
},
"dependencies": {
"next": "16.2.11",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "8.5.25",
"tailwindcss": "^4.0.0",
"typescript": "^5"
},
"overrides": {
"postcss": "8.5.25",
"sharp": "0.35.3"
}
}
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}