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

Structured publishing registers metrics, creates a campaign and run, then appends iterations containing timestamped measurements and values.

cURL

# Register a metric, then retain the returned id.
curl -X POST https://benchmark.infinity.inc/api/v1/metrics \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"decode-throughput","name":"Decode throughput",
       "unit":"token/s","direction":"higher_is_better"}'

curl -X POST https://benchmark.infinity.inc/api/v1/campaigns \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"qwen-neuralchip-x1","name":"Qwen on NeuralChip X1",
       "model":"Qwen 3.5 9B","hardware":"NeuralChip X1"}'

# Use the returned campaign, run, and metric UUIDs in subsequent requests.
curl -X POST https://benchmark.infinity.inc/api/v1/campaigns/$CAMPAIGN_ID/runs \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"July run","workload":"interactive-decode",
       "harness_version":"1.12.0"}'

curl -X POST https://benchmark.infinity.inc/api/v1/runs/$RUN_ID/iterations \
  -H "Authorization: Bearer $HUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"iteration_number":0,"phase":"baseline",
       "observed_at":"2026-07-22T12:00:00.000Z",
       "measurements":[{"name":"interactive-decode",
         "observed_at":"2026-07-22T12:00:00.000Z",
         "values":[{"metric_id":"'"$METRIC_ID"'","unit":"token/s","value":120}]}]}'

TypeScript SDK

@infinity/benchmark-hub-client is an ESM-only package exposing the typed, dependency-free HubClient. Identical iteration retries return the persisted iteration with created set to false.

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

const hub = new HubClient("https://benchmark.infinity.inc", process.env.HUB_API_KEY!);
const metric = await hub.registerMetric({
  key: "decode-throughput", name: "Decode throughput",
  unit: "token/s", direction: "higher_is_better",
});
const campaign = await hub.resolveCampaign("qwen-neuralchip-x1")
  ?? await hub.createCampaign({
    slug: "qwen-neuralchip-x1", name: "Qwen on NeuralChip X1",
    model: "Qwen 3.5 9B", hardware: "NeuralChip X1",
  });
const run = await hub.createRun(campaign.id, {
  name: "July run", workload: "interactive-decode", harness_version: "1.12.0",
});
await hub.transitionRun(run.id, { status: "active" });

const observedAt = new Date().toISOString();
const { iteration, created } = await hub.appendIteration(run.id, {
  iteration_number: 0, phase: "baseline", observed_at: observedAt,
  measurements: [{ name: "interactive-decode", observed_at: observedAt,
    values: [{ metric_id: metric.id, unit: metric.unit, value: 120 }],
  }],
});
console.log(created ? "created" : "identical retry", iteration.id);

Python

Publishers on ML hardware don't need the SDK; the structured API accepts plain JSON over HTTP.

import os, httpx

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

metric = httpx.post(f"{HUB}/api/v1/metrics", headers=h, json={
    "key": "decode-throughput", "name": "Decode throughput",
    "unit": "token/s", "direction": "higher_is_better",
}).raise_for_status().json()
campaign = httpx.post(f"{HUB}/api/v1/campaigns", headers=h, json={
    "slug": "qwen-neuralchip-x1", "name": "Qwen on NeuralChip X1",
    "model": "Qwen 3.5 9B", "hardware": "NeuralChip X1",
}).raise_for_status().json()
run = httpx.post(f"{HUB}/api/v1/campaigns/{campaign['id']}/runs", headers=h,
                 json={"name": "July run", "workload": "interactive-decode",
                       "harness_version": "1.12.0"}).raise_for_status().json()

observed_at = "2026-07-22T12:00:00.000Z"
iteration = httpx.post(f"{HUB}/api/v1/runs/{run['id']}/iterations", headers=h, json={
    "iteration_number": 0, "phase": "baseline", "observed_at": observed_at,
    "measurements": [{"name": "interactive-decode", "observed_at": observed_at,
                      "values": [{"metric_id": metric["id"],
                                  "unit": metric["unit"], "value": 120}]}],
}).raise_for_status().json()
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.