How it works
This is a small, complete, cross-provider LLM benchmark. About two thousand lines, no framework, and every part of it is here to solve a problem you will hit too if you build one. This page walks the pipeline in order.
Decide what you are asking
Questions live in a JSON file, not in code. Adding one is a data change, and the schema enforces that every question ends with the same instruction — which is what makes the compliance metric mean anything.
Decide who you are asking
Model IDs live here and nowhere else. Every entry records the documentation page its ID was verified against and the date its pricing was read, because both go stale and a cost estimate from an undated table is a number with no meaning.
Write one adapter per vendor
Each adapter turns one vendor’s HTTP API into the same small shape. It receives its API key and its fetch by injection, so it can be tested with no network and later run in a browser.
Share the boring parts
Timeouts, retries with jittered backoff, and error classification are identical across every vendor, so they are written once. An adapter that called fetch directly would reimplement this badly.
Ask everything, several times, politely
The runner asks every model every question three times, with bounded concurrency and never more than one in-flight request per provider. That second constraint matters: three simultaneous calls to one vendor invites a rate limit, and a retried request measures your own impatience rather than the provider.
Write the answer down, permanently
One JSON file per ISO week, validated against a versioned schema and committed. Re-running a week corrects that edition rather than creating a second one.
Do it again next week without being asked
A scheduled GitHub Action runs the benchmark with keys from repository secrets, commits the new data, and triggers a redeploy. Nobody runs the benchmark by hand.
The whole interface a provider has to satisfy
This is the entire abstraction. Adding a vendor means writing one function that satisfies it. Everything a provider might be tempted to own — model IDs, pricing, retry policy — already lives somewhere shared.
export interface ProviderAdapter {
/** Stable id matching `models.json` and the credential map. */
readonly id: string
/** Human name for logs and error messages. */
readonly displayName: string
/** Ask the model. Throws {@link ProviderError} on any failure. */
complete(request: CompleteRequest, context: AdapterContext): Promise<CompleteResult>
}From src/providers/types.ts, read from source when this page was built.
What an adapter actually does
The middle of the Anthropic adapter, which is the one to read first. It consumes a server-sent-event stream, accumulates the text, and marks the moment the first content token arrives — which is the only way time-to-first-token can be measured at all.
Note the comment on message_delta. Anthropic documents those token counts as cumulative, so the last one wins. That is the kind of detail you only learn by reading the documentation carefully or by getting it wrong for a month.
for await (const event of readSseJson<AnthropicEvent>(response)) {
switch (event.type) {
case 'message_start':
// Carries input_tokens and the cache counts. Output tokens here
// are a running start, superseded by message_delta below.
usage = { ...usage, ...event.message?.usage }
break
case 'content_block_delta':
if (event.delta?.type === 'text_delta' && event.delta.text) {
// The first content token: this is the moment ttfbMs measures.
// Marking is idempotent, so calling it per delta is fine.
measurement.markFirstToken()
context.onFirstToken?.()
text += event.delta.text
}
break
case 'message_delta':
// Anthropic documents these counts as *cumulative*, so the last
// message_delta holds the authoritative totals. Merging rather
// than replacing keeps input_tokens from message_start when a
// delta omits it.
usage = { ...usage, ...event.usage }
break
case 'error':
// An error can arrive mid-stream, after a 200. The HTTP layer has
// already returned by then, so it has to be caught here.
throw new ProviderError(
'server',
`Anthropic stream error: ${event.error?.message ?? event.error?.type ?? 'unknown'}`,
)
}
}From src/providers/anthropic.ts.
Try it without an API key
The entire pipeline runs from recorded fixtures. Everything except the network call is real — classification, aggregation, cost estimation, schema validation, and this site:
npm run bench -- run --mock
npm run devWhat the numbers do and do not mean is covered on the methodology page. Adding a model or a provider is described on add a model.