Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/app/answers/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { cleanLeftoverTokens } from "@/lib/answers-template";
import { Breadcrumb } from "@/components/breadcrumb";
import { Pill } from "@/components/pill";
import { ProviderLogo } from "@/components/provider-logo";
import { fmtValue, fmtUnit, unitSuffix } from "@/lib/format";
import { fmtAsOfUtc, fmtValue, fmtUnit, unitSuffix } from "@/lib/format";
import { isRegion } from "@/lib/brand";
import { SITE } from "@/data/site";
import {
Expand Down Expand Up @@ -92,6 +92,7 @@ export default async function AnswerPage({
// YAML's slug reference) fall through to a neutral fallback via
// cleanLeftoverTokens so a placeholder string never reaches the SERP.
const render = (s: string) => cleanLeftoverTokens(renderTemplate(s, bench));
const asOfUtc = fmtAsOfUtc(bench.lastRunAt);
const shortAnswer = render(ans.short_answer);
const intro = render(ans.intro);
const methodology = render(ans.methodology);
Expand Down Expand Up @@ -187,6 +188,17 @@ export default async function AnswerPage({

<div className="mt-6 max-w-3xl border-y border-rule py-5 text-base sm:text-lg leading-relaxed text-ink">
{shortAnswer}
{/* Dated freshness stamp right next to the quotable sentence,
mirroring the "Data as of ... UTC" line on the bench pages.
Answer engines quote a claim far more readily when the date
of measurement sits beside it. Uses the referenced bench's
lastRunAt (real data timestamp), not build time. */}
{asOfUtc && (
<p className="mt-3 text-[11px] text-ink-faint">
Data as of <time dateTime={bench.lastRunAt}>{asOfUtc}</time>,
refreshed continuously.
</p>
)}
</div>

<p className="mt-6 max-w-3xl text-base leading-relaxed text-ink-soft">
Expand Down
14 changes: 12 additions & 2 deletions src/app/api/citable/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getBenchmarks } from "@/data/benchmarks";
import { SITE } from "@/data/site";
import { AllBenchmarksDraftError } from "@/lib/spec";
import { citeBundle, fieldValue, leader, headlineSentence } from "@/lib/citation";
import { valueInDeclaredUnit } from "@/lib/format";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";

export const runtime = "nodejs";
Expand Down Expand Up @@ -53,19 +54,28 @@ export async function GET(req: Request) {
// from an undersized field. The headline sentence is rewritten to
// "insufficient data" by headlineSentence above.
const insufficient = b.dataConfidence === "insufficient";
// `value` and `leader.value` are published in the declared `unit`.
// Latency benches with unit "s" store ms internally (fmtUnit
// convention); valueInDeclaredUnit converts so the JSON never claims
// 645 seconds for a 645 ms head lag.
const raw = insufficient ? null : fieldValue(b);
return {
slug: b.slug,
title: b.title,
category: b.category,
metric: b.metric,
unit: b.unit,
status: b.status,
value: insufficient ? null : fieldValue(b),
value: raw == null ? null : valueInDeclaredUnit(raw, b.unit),
leader:
insufficient
? null
: top
? { name: top.name, slug: top.slug, value: top.value }
? {
name: top.name,
slug: top.slug,
value: valueInDeclaredUnit(top.value, b.unit),
}
: null,
sampleSize: b.sampleSize,
expectedN: b.expectedN,
Expand Down
71 changes: 67 additions & 4 deletions src/app/api/openapi.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export async function GET() {
openapi: "3.1.0",
info: {
title: "OpenChainBench API",
version: "1.0.0",
description: SITE.description,
version: "1.1.0",
description: `${SITE.description} An MCP server (Streamable HTTP, POST only) is also available at ${SITE.url}/api/mcp/mcp exposing list_benchmarks, get_benchmark and query_prom tools; see ${SITE.url}/mcp for install instructions.`,
license: { name: "CC-BY-4.0", url: "https://creativecommons.org/licenses/by/4.0/" },
},
servers: [{ url: SITE.url }],
Expand Down Expand Up @@ -67,6 +67,50 @@ export async function GET() {
},
},
},
"/api/llm-context": {
get: {
summary:
"All benchmarks, rankings and methodology as one Markdown document, ready to paste into a system prompt.",
operationId: "get_llm_context",
responses: {
"200": {
description: "OK",
content: { "text/markdown": { schema: { type: "string" } } },
},
"503": { description: "Benchmarks temporarily unavailable" },
},
},
},
"/api/freshness": {
get: {
summary:
"Lightweight freshness probe: last resolved data timestamp (epoch ms) per benchmark slug.",
operationId: "get_freshness",
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Freshness" },
},
},
},
},
},
},
"/api/search/featured": {
get: {
summary:
"Slim featured-leaders payload (live leaders + trending benches) that feeds the site search dialog.",
operationId: "get_featured_leaders",
responses: {
"200": {
description: "OK",
content: { "application/json": { schema: { type: "object" } } },
},
},
},
},
},
components: {
schemas: {
Expand All @@ -85,20 +129,39 @@ export async function GET() {
title: { type: "string" },
metric: { type: "string" },
unit: { type: "string" },
value: { type: "number", nullable: true },
value: {
type: "number",
nullable: true,
description: "Current leading value, expressed in `unit`.",
},
headline: { type: "string" },
url: { type: "string", format: "uri" },
api: { type: "string", format: "uri" },
ogImage: { type: "string", format: "uri" },
asOf: { type: "string", format: "date-time" },
},
},
Freshness: {
type: "object",
properties: {
now: { type: "integer", description: "Server epoch ms at response time." },
freshness: {
type: "object",
additionalProperties: { type: "integer" },
description: "Benchmark slug to last data timestamp (epoch ms).",
},
},
},
Stat: {
type: "object",
properties: {
slug: { type: "string" },
title: { type: "string" },
value: { type: "number", nullable: true },
value: {
type: "number",
nullable: true,
description: "Current leading value, expressed in `unit`.",
},
unit: { type: "string" },
rankings: { type: "array" },
sparkline: { type: "array", items: { type: "number" } },
Expand Down
11 changes: 9 additions & 2 deletions src/app/api/stat/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
leader,
sparklineFor,
} from "@/lib/citation";
import { valueInDeclaredUnit } from "@/lib/format";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
import { SLUG_RE } from "@/lib/slug";

Expand Down Expand Up @@ -45,6 +46,9 @@ export async function GET(

const top = leader(b);
const insufficient = b.dataConfidence === "insufficient";
// Publish value + leader.value in the declared unit. Unit "s" benches
// store ms internally (fmtUnit convention); do not leak that here.
const raw = insufficient ? null : fieldValue(b);
const payload = {
slug: b.slug,
title: b.title,
Expand All @@ -59,8 +63,11 @@ export async function GET(
// leader; the headline is rewritten by headlineSentence so the
// agent / journalist reads "insufficient data" instead of quoting
// a number drawn from undersized samples.
value: insufficient ? null : fieldValue(b),
leader: insufficient ? null : top,
value: raw == null ? null : valueInDeclaredUnit(raw, b.unit),
leader:
insufficient || !top
? null
: { ...top, value: valueInDeclaredUnit(top.value, b.unit) },
rankings: b.results
.filter((r) => r.ms.p50 > 0)
// Drop "insufficient" rows from the machine-readable ranking too:
Expand Down
5 changes: 4 additions & 1 deletion src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
isInsufficient,
leader,
} from "@/lib/citation";
import { valueInDeclaredUnit } from "@/lib/format";
import { capDescription } from "@/lib/seo-text";
import { getBenchCreatedAt } from "@/lib/seo/bench-dates";
import { SITE } from "@/data/site";
Expand Down Expand Up @@ -332,7 +333,9 @@ export default async function BenchmarkPage({
metric: benchmark.metric,
metricUnit: benchmark.unit,
leaderName: currentLeader.name,
leaderValue: currentLeader.value,
// Observation.measuredValue must be in unitText. Unit "s" benches
// store ms internally; convert before publishing to JSON-LD.
leaderValue: valueInDeclaredUnit(currentLeader.value, benchmark.unit),
temporalCoverage: `${getBenchCreatedAt(benchmark.slug).toISOString()}/${benchmark.lastRunAt}`,
observationDate: benchmark.lastRunAt,
url: benchmarkUrl,
Expand Down
2 changes: 1 addition & 1 deletion src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function GET() {
lines.push(`- [Citable index (JSON)](${SITE.url}/api/citable): flat list of all benchmarks with current values, ready for one-shot lookup.`);
lines.push(`- [LLM context (Markdown)](${SITE.url}/api/llm-context): all ${benches.length} benchmarks + rankings + methodology in one Markdown blob, ready to paste into a system prompt.`);
lines.push(`- [OpenAPI schema](${SITE.url}/api/openapi.json): full description of every endpoint.`);
lines.push(`- [MCP server](${SITE.url}/api/mcp/mcp): exposes \`list_benchmarks\`, \`get_benchmark\`, \`query_prom\` tools + \`openchainbench://benchmark/{slug}\` resources over Streamable HTTP. See [/mcp](${SITE.url}/mcp) for install instructions (Claude Desktop, Cursor, generic clients).`);
lines.push(`- [MCP server docs](${SITE.url}/mcp): install instructions (Claude Desktop, Cursor, generic clients) for the MCP server at ${SITE.url}/api/mcp/mcp, which exposes \`list_benchmarks\`, \`get_benchmark\`, \`query_prom\` tools + \`openchainbench://benchmark/{slug}\` resources over Streamable HTTP (JSON-RPC via POST; the endpoint is not browsable with GET).`);
lines.push("");
lines.push(`## Benchmarks`);
lines.push("");
Expand Down
15 changes: 13 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getBenchmarksSafe } from "@/data/benchmarks";
import { HeroRadar } from "@/components/hero-radar";
import { HomeBenchTable } from "@/components/home-bench-table";
import { LiveDashboard } from "@/components/live/dashboard";
import { GLOBAL_DATASET_JSONLD } from "@/lib/dataset-jsonld";
import { buildGlobalDatasetJsonLd } from "@/lib/dataset-jsonld";
import { safeJsonLd } from "@/lib/jsonld";

export const revalidate = 60;
Expand Down Expand Up @@ -35,6 +35,15 @@ export const metadata: Metadata = {
export default async function HomePage() {
const benchmarks = await getBenchmarksSafe();

// Same timestamp source the bench pages use for Dataset.dateModified:
// the harness lastRunAt (real data timestamp, not build time). The
// newest one across the catalog dates the site-wide Dataset node.
const latestRunAt = benchmarks
.map((b) => b.lastRunAt)
.filter((iso) => iso && !Number.isNaN(new Date(iso).getTime()))
.sort()
.pop();

return (
<article className="mx-auto max-w-[1400px] px-4 sm:px-6 py-10 sm:py-14 space-y-14 sm:space-y-20">
{/* Site-wide schema.org/Dataset entry. Points Google Dataset Search,
Expand All @@ -46,7 +55,9 @@ export default async function HomePage() {
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: serialized via safeJsonLd
dangerouslySetInnerHTML={{ __html: safeJsonLd(GLOBAL_DATASET_JSONLD) }}
dangerouslySetInnerHTML={{
__html: safeJsonLd(buildGlobalDatasetJsonLd(latestRunAt)),
}}
/>

{/* Hero */}
Expand Down
14 changes: 1 addition & 13 deletions src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import { CountLeaderboard } from "@/components/count-leaderboard";
import { SummaryStat } from "@/components/summary-stat";
import { ViewSwitcher } from "@/components/view-switcher";
import { fmtUnit } from "@/lib/format";
import { fmtAsOfUtc, fmtUnit } from "@/lib/format";
import { computeFieldStats } from "@/lib/stats";
import { defaultViewFor, viewsForBenchmark } from "@/lib/views";
import { useViewPreference } from "@/hooks/use-view-preference";
Expand Down Expand Up @@ -62,18 +62,6 @@
);
}

/** "2026-07-06 14:00 UTC" from the bench's lastRunAt ISO string. Fixed
* UTC rendering so server and client HTML agree (no hydration drift)
* and answer engines get an unambiguous freshness stamp. */
function fmtAsOfUtc(iso: string): string | null {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return null;
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(
d.getUTCDate(),
)} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
}

/** Mutate `url.searchParams` to keep one dimension param in sync.
* Removes the param when the value is the first option (the implicit
* default) so canonical URLs stay short. */
Expand Down Expand Up @@ -540,7 +528,7 @@
// not only on the timeseries view. Providers the panel has no value
// for (book could not fill the tier) drop out of the ranking, which
// is the skipped-not-extrapolated rule made visible.
const panelViewBenchmark = useMemo(() => {

Check failure on line 531 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
if (!activePanel) return viewBenchmark;
const vals = activePanel.values ?? {};
return {
Expand Down
23 changes: 19 additions & 4 deletions src/lib/dataset-jsonld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ export const HF_DATASET_URL =
export const ZENODO_CONCEPT_DOI = "10.5281/zenodo.20800311";
export const ZENODO_CONCEPT_URL = `https://doi.org/${ZENODO_CONCEPT_DOI}`;

/** Latest parquet headlines snapshot. `LATEST` is the convention used by
* the HF publisher pipeline so downstream consumers can pin to a date or
* follow head. */
/** Latest parquet headlines snapshot via Hugging Face's auto-converted
* parquet API. The repo itself only holds dated partitions
* (headlines/snapshot_date=YYYY-MM-DD/); the previously linked
* snapshot_date=LATEST path never existed and 404'd. This endpoint
* always tracks the head of main, so it stays valid without rebuilds. */
export const HF_HEADLINES_LATEST =
"https://huggingface.co/datasets/OpenChainBench/benchmarks/resolve/main/headlines/snapshot_date=LATEST/part-0.parquet";
"https://huggingface.co/api/datasets/OpenChainBench/benchmarks/parquet/headlines/train/0.parquet";

/** CC-BY-4.0 license URL. Matches the per-row license surfaced in
* /api/citable and the footer on every page. */
Expand Down Expand Up @@ -68,7 +70,20 @@ const CREATOR_PUBLISHER = {
* Site-wide Dataset entry. Emitted on the home page so Google Dataset
* Search and Perplexity have a single canonical record pointing at both
* the live JSON index (/api/citable) and the parquet mirror on HF.
*
* `buildGlobalDatasetJsonLd` folds in `dateModified` from the newest
* bench lastRunAt so the home Dataset carries the same freshness signal
* the per-bench Dataset nodes already emit.
*/
export function buildGlobalDatasetJsonLd(
dateModified?: string,
): Record<string, unknown> {
return {
...GLOBAL_DATASET_JSONLD,
...(dateModified ? { dateModified } : {}),
};
}

export const GLOBAL_DATASET_JSONLD = {
"@context": "https://schema.org",
"@type": "Dataset",
Expand Down
24 changes: 24 additions & 0 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
/** Convert a stored value into the bench's declared unit for machine
* readable payloads. Latency benches with unit "s" store milliseconds
* by convention (see fmtUnit below, which divides by 1000 before
* rendering). That convention must not leak into public JSON where
* agents read `value` + `unit` literally: aggregator-head-lag shipped
* {"value":645,"unit":"s"} while the headline said "0.6 s". Every
* other unit stores exactly what it declares. */
export function valueInDeclaredUnit(value: number, unit: string): number {
return unit === "s" ? value / 1000 : value;
}

/** "2026-07-06 14:00 UTC" from an ISO timestamp. Fixed UTC rendering so
* server and client HTML agree (no hydration drift) and answer engines
* get an unambiguous freshness stamp. Shared by benchmark-body and the
* answers pages so the dated line reads identically everywhere. */
export function fmtAsOfUtc(iso: string): string | null {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return null;
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(
d.getUTCDate(),
)} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
}

export function fmtUnit(value: number, unit: string) {
if (!Number.isFinite(value)) return "-";
if (unit === "pct") return formatPercent(value);
Expand Down
Loading