Benchmark Hub

Reference

Ingest API

Hardware-bound inference libraries publish declarative benchmark specs to the Hub over plain HTTP + JSON. A spec is a list of whitelisted UI nodes, never raw markup, so a publisher describes what to show and the Hub decides how to render it. This page documents every endpoint, its schemas and errors, and the full component whitelist.

Overview

  • Base path /api/v1. All requests and responses are JSON.
  • Two resources, projects and their benchmarks. Both writes are upserts keyed on slug: POST the same slug again to update in place.
  • There is intentionally no delete endpoint. Deletion is a human, UI-only action; the API can only create, update, and read.
  • Every benchmark spec is validated against the component whitelist before storage. Nothing partial is ever persisted.

Authentication

Authenticate with a Hub-issued API key sent as a Bearer token on every request:

Authorization: Bearer <your-api-key>

The key is an opaque random token; the Hub stores only its SHA-256 hash, so a database read can never reconstruct a live credential. A missing or unrecognized token returns 401. Keys belong to one of two account types, library (automated publishers) or human, which the authorization rules below distinguish.

Authorization

Authorization is enforced server-side per request. The rules:

ActionWho may do it
Read any project or benchmarkAny valid API key
Create a new projectAny valid API key; the creator becomes the owner
Update a project / publish a benchmarkThe project owner, or any human account
Delete anythingHumans only, and only via the UI; no API route exists

This is a small, trusted set of key-holders by design: every human is an admin, and there is no per-owner isolation between library keys beyond the ownership check on update. A write you aren't allowed to make returns 403.

Endpoints

GET/api/v1/projects

List every project, ordered by name.

Auth
Any valid API key
Returns
ProjectOut[]
POST/api/v1/projects

Create a project, or update one you own. Keyed on slug (upsert).

Auth
Any valid API key to create; owner or a human to update
Body
ProjectIn
Returns
ProjectOut
  • Creating a new slug makes the caller its owner.
  • Ownership is never reassigned on a later update.
GET/api/v1/projects/{project}/benchmarks

List a project's benchmarks, newest run first.

Auth
Any valid API key
Returns
BenchmarkOut[]
  • 404 if the project slug does not exist.
POST/api/v1/projects/{project}/benchmarks

Publish a benchmark into a project. Keyed on slug (upsert).

Auth
Project owner or a human
Body
BenchmarkIn
Returns
BenchmarkOut
  • The render spec is validated against the whitelist before anything is stored; an invalid node returns 422 and persists nothing.
  • Each publish stamps last_run_at with the server time.

{project}in a path is the parent project's slug.

Schemas

Request bodies are strict; any unexpected key is rejected with a 422. A ? marks an optional field.

ProjectIn

request
FieldTypeNotes
slugstringURL-safe id. Pattern ^[a-z0-9][a-z0-9-]{0,63}$ (≤ 64 chars).
namestringDisplay name, 1–200 chars.
description?string | nullOptional, ≤ 2000 chars.
hardware?string | nullHardware side of the pair, ≤ 200 chars (e.g. "Apple M2"). Set it: a project is one hardware × model pair.
model?string | nullModel side of the pair, ≤ 200 chars (e.g. "Whisper").

Extra keys are rejected (strict schema). Convention: one project per hardware × model pair; each run publishes a new benchmark into it.

ProjectOut

response
FieldTypeNotes
idstringServer-assigned UUID.
slugstringThe project slug.
namestringDisplay name.
descriptionstring | nullMay be null.
hardwarestring | nullMay be null.
modelstring | nullMay be null.
owner_idstringUUID of the identity that owns the project.

BenchmarkIn

request
FieldTypeNotes
slugstringURL-safe id, same pattern as a project slug.
namestringDisplay name, 1–200 chars.
hardware?string | nullOptional hardware label, ≤ 200 chars (e.g. "Apple M3 Max").
specNode[]The render spec: an ordered array of whitelisted nodes (≤ 2000). See the component whitelist below.

Extra keys are rejected (strict schema).

BenchmarkOut

response
FieldTypeNotes
idstringServer-assigned UUID.
project_idstringUUID of the parent project.
slugstringThe benchmark slug.
namestringDisplay name.
hardwarestring | nullMay be null.
last_run_atstringISO-8601 timestamp of the most recent publish.

Errors

Errors use conventional HTTP status codes and a JSON body with a message. Validation failures (422) additionally carry an errors array of issues, each pointing at the offending path.

StatusWhenBody
401Missing or invalid Bearer token.{ "message": string }
403Authenticated, but not allowed to write this project/benchmark.{ "message": string }
404Referenced project (or benchmark) does not exist.{ "message": string }
422Body isn't valid JSON, fails the request schema, exceeds the size limit, or the spec fails whitelist validation.{ "message": string, "errors": Issue[] }
500Unexpected server error.{ "message": "internal error" }

A 422 from a spec that fails whitelist validation looks like:

{
  "message": "invalid render spec",
  "errors": [
    { "code": "custom", "path": ["3", "rows", 2],
      "message": "row 2 has 4 cells but there are 3 headers" }
  ]
}

Limits

  • Request body: up to 8 MB. A larger declared Content-Length is rejected with 422 before parsing.
  • Spec size: up to 2000 top-level nodes.
  • Nesting depth: layout containers (row, columns, tabs) may nest up to 32 levels deep.
  • Images: src must be an http(s):// URL or a data:image/URI, up to 5 MB of characters.
  • Per-field caps (string lengths, array sizes) are listed with each node in the whitelist below.

Component whitelist

A spec is a JSON array of nodes. Each node is an object discriminated on a literal type. Only the types below are accepted; any other type, or any extra key on a node, is rejected. There is no raw-HTML or script node; this whitelist is the injection boundary.

Text

header

A section heading.

  • text: string (1–300)
  • level?: int 1–6 (default 2)
paragraph

A block of prose.

  • text: string (1–20000)
note

A callout box.

  • text: string (1–20000)
  • variant?: "info" | "warning" | "success" | "error" (default "info")
  • title?: string (≤ 200)
code_block

A fenced code block.

  • code: string (≤ 100000)
  • language?: string (≤ 40, default "text")
  • caption?: string (≤ 300)
divider

A horizontal rule.

  • (no fields)

Data

table

A data table. Every row must have exactly as many cells as there are headers.

  • headers: string[] (1–50)
  • rows: Cell[][] (≤ 10000; each row length = headers length)
  • caption?: string (≤ 300)
  • Cell = string | number | boolean | null
key_value

A definition list.

  • items: { key: string (1–200), value: string (≤ 2000) }[] (1–100)
  • title?: string (≤ 200)
kpi_row

A row of metric tiles.

  • metrics: Metric[] (1–12)
  • Metric = { label: string (1–120), value: string (≤ 60), unit?: string (≤ 30), delta?: string (≤ 60), delta_direction?: "up" | "down" | "neutral" (default "neutral") }
image

An image. No arbitrary markup; src is constrained.

  • src: string, an http(s):// URL or a data:image/ URI (≤ 5,000,000 chars)
  • alt?: string (≤ 300)
  • caption?: string (≤ 300)

Charts

line_chart

One or more x/y line series.

  • series: { name: string (1–120), x: number[] (1–100000), y: number[] (same length as x) }[] (1–30)
  • x_label?: string (default "x")
  • y_label?: string (default "y")
  • title?: string (≤ 200)
bar_chart

Grouped bars over shared categories.

  • categories: string[] (1–1000)
  • series: { name: string, values: number[] (same length as categories, ≤ 1000) }[] (1–30)
  • x_label?: string (default "")
  • y_label?: string (default "")
  • title?: string (≤ 200)
scatter_chart

Labeled x/y point clouds.

  • series: { name: string, points: { x: number, y: number, label?: string }[] (1–100000) }[] (1–30)
  • x_label?: string (default "x")
  • y_label?: string (default "y")
  • title?: string (≤ 200)
roofline_chart

A roofline model: peak compute + bandwidth ceilings with plotted kernels.

  • peak_flops: number (> 0)
  • peak_bandwidth: number (> 0)
  • points?: { label: string (1–120), intensity: number (> 0), performance: number (> 0) }[] (≤ 1000, default [])
  • title?: string (≤ 200)

Layout (recursive)

row

Lay children out horizontally.

  • children: Node[] (1–12)
columns

Fixed columns, each a stack of nodes.

  • columns: Node[][] (1–6)
tabs

Tabbed panels.

  • tabs: { label: string (1–120), children: Node[] (1–200) }[] (1–20)

Fields marked ?are optional; every default shown is what the Hub applies when the field is omitted. Charts enforce cross-field consistency (e.g. a line series' x and y must be the same length) and report a 422 pointing at the mismatch.

Examples

The same publish flow: create a project, then publish a benchmark into it.

cURL

# 1. Register (or update) the pair-project. You own any slug you create.
#    One project per hardware × model pair.
curl -X POST https://benchmark.infinity.inc/api/v1/projects \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"apple-m2-whisper","name":"Apple M2 · Whisper",
       "hardware":"Apple M2","model":"Whisper"}'

# 2. Publish a run as a new benchmark. Its spec is validated before storage.
curl -X POST https://benchmark.infinity.inc/api/v1/projects/apple-m2-whisper/benchmarks \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "20260709-181500-fp16",
    "name": "2026-07-09 18:15 · fp16",
    "hardware": "Apple M2 · 16 GB · macOS 15",
    "spec": [
      {"type": "header", "text": "MLX Whisper vs baselines", "level": 1},
      {"type": "kpi_row", "metrics": [
        {"label": "tok/s", "value": "142", "delta": "+18%", "delta_direction": "up"}]},
      {"type": "bar_chart", "categories": ["mine", "llama.cpp"],
       "series": [{"name": "tok/s", "values": [142, 120]}]}
    ]
  }'

TypeScript SDK

@infinity/benchmark-hub-client ships a fluent SpecBuilder (type-checked against this whitelist) and a dependency-free HubClient.

import { SpecBuilder, HubClient } from "@infinity/benchmark-hub-client";

const spec = new SpecBuilder()
  .header("MLX Whisper vs baselines", 1)
  .kpiRow([{ label: "tok/s", value: "142", delta: "+18% vs llama.cpp", delta_direction: "up" }])
  .barChart(["mine", "llama.cpp"], [{ name: "tok/s", values: [142, 120] }])
  .roofline(7000, 400, [{ label: "gemm", intensity: 40, performance: 3800 }])
  .validate(); // optional: fail locally against the whitelist before publishing

const hub = new HubClient("https://benchmark.infinity.inc", process.env.HUB_API_KEY!);

await hub.publishProject("apple-m2-whisper", "Apple M2 · Whisper", {
  hardware: "Apple M2", model: "Whisper",
});
await hub.publishBenchmark(
  "apple-m2-whisper", "20260709-181500-fp16", "2026-07-09 18:15 · fp16",
  spec, "Apple M2 · 16 GB · macOS 15",
);

Python

Publishers on ML hardware don't need the SDK; the API is plain HTTP. Build the spec as a list of dicts and POST it.

import os, httpx

HUB = "https://benchmark.infinity.inc"
h = {"Authorization": f"Bearer {os.environ['HUB_API_KEY']}",
     "Content-Type": "application/json"}

httpx.post(f"{HUB}/api/v1/projects", headers=h,
           json={"slug": "apple-m2-whisper", "name": "Apple M2 · Whisper",
                 "hardware": "Apple M2", "model": "Whisper"})

spec = [
    {"type": "header", "text": "MLX Whisper vs baselines", "level": 1},
    {"type": "kpi_row", "metrics": [
        {"label": "tok/s", "value": "142", "delta": "+18%", "delta_direction": "up"}]},
    {"type": "bar_chart", "categories": ["mine", "llama.cpp"],
     "series": [{"name": "tok/s", "values": [142, 120]}]},
]
httpx.post(f"{HUB}/api/v1/projects/apple-m2-whisper/benchmarks", headers=h,
           json={"slug": "20260709-181500-fp16", "name": "2026-07-09 18:15 · fp16",
                 "spec": spec, "hardware": "Apple M2 · 16 GB · macOS 15"})
Something look wrong or missing? The contract lives in app/api/v1, lib/api/schemas.ts, and lib/spec/schema.ts, and this page is written to mirror them. Back to the Hub.