Every byte of test data in Inference Atlas is authored in this repository —
synthetic, generated by the scripts in _gen/, and MIT
licensed (SPEC §0.6). No third-party corpus is mirrored here, nothing was
scraped, and no model output was used as data. That is what lets anyone fork the
project, run the same workloads and compare numbers without a licence question.
A dataset is pinned to the workloads that reference it. Published datasets are
immutable: changing one means a new -v2 id, because two results are only
comparable when they saw the same bytes.
Index
| id | kind | rows | size | generator | default scorer |
|---|---|---|---|---|---|
prompts-mixed-v1 |
prompts | 600 | 3.14 MB | gen_prompts_mixed.py |
– |
prompts-shared-prefix-v1 |
prompts | 100 | 2.88 MB | gen_prompts_shared_prefix.py |
– |
prompts-code-v1 |
prompts | 150 | 449 KB | gen_prompts_code.py |
– |
haystack-v1 |
haystack | 32 | 1007 KB | gen_haystack.py |
– |
eval-math-v1 |
eval | 130 | 40 KB | gen_eval_math.py |
numeric |
eval-reasoning-v1 |
eval | 120 | 56 KB | gen_eval_reasoning.py |
exact |
eval-code-v1 |
eval | 140 | 164 KB | gen_eval_code.py |
code_exec |
eval-knowledge-v1 |
eval | 130 | 49 KB | gen_eval_knowledge.py |
mc |
eval-instruction-v1 |
eval | 104 | 65 KB | gen_eval_instruction.py |
instruction |
eval-json-v1 |
eval | 110 | 62 KB | gen_eval_json.py |
json |
eval-tools-v1 |
eval | 80 | 149 KB | gen_eval_tools.py |
json |
eval-vision-v1 |
eval | 60 | 141 KB | gen_eval_vision.py |
exact (+ images) |
eval-multilingual-v1 |
eval | 80 | 39 KB | gen_eval_multilingual.py |
contains |
eval-longctx-v1 |
eval | 100 | 100 KB | gen_eval_longctx.py |
needle |
eval-format-v1 |
eval | 30 | 8 KB | gen_eval_format.py |
exact |
eval-math-v2 |
eval | 140 | 54 KB | gen_eval_math_v2.py |
numeric |
eval-reasoning-v2 |
eval | 140 | 81 KB | gen_eval_reasoning_v2.py |
exact |
eval-knowledge-v2 |
eval | 151 | 63 KB | gen_eval_knowledge_v2.py |
mc |
eval-science-v2 |
eval | 120 | 49 KB | gen_eval_science_v2.py |
numeric |
eval-commonsense-v2 |
eval | 116 | 75 KB | gen_eval_commonsense_v2.py |
mc |
eval-security-v2 |
eval | 111 | 63 KB | gen_eval_security_v2.py |
mc |
eval-longgen-integrity-v1 |
eval | 36 | 1.03 MB | gen_eval_longgen_integrity.py |
integrity |
eval-longgen-integrity-v1 (2026-09-02) is a different axis again. Every suite above
asks a short question, so a serving build that corrupts roughly one token in several
thousand scores 100 % on all of them: the answer ends before the defect has room to
appear. This one asks for 36 code generations of 150 to 290 lines each and scores them
mechanically for spliced tokens — an identifier or number welded together from two
fragments — so accuracy reads as "share of long generations that came back intact".
The -v2 rows are the second-generation eval wave (2026-09-01): the v1
capability evals saturated — several models score 98–100 % on knowledge, math,
reasoning, tools and format — so v2 raises the difficulty ceiling.
eval-math-v2, eval-reasoning-v2 and eval-knowledge-v2 supersede their v1
ids (which stay published and immutable); eval-science-v2 (applied
physics/chemistry with every constant pinned in the prompt) and
eval-commonsense-v2 (goal-tracking traps, altered classic riddles, false
premises, literal-text questions) and eval-security-v2 (defensive cyber
security: toy crypto, CIDR arithmetic, vulnerability classification, log
forensics, incident-response traps) are new suites, versioned -v2 to mark
the wave they belong to.
Licence for all of the above: MIT. Total, including the generator scripts:
about 11.5 MB against a 25 MB budget, which _gen/check.py prints and enforces.
Validate the whole corpus:
uv run datasets/_gen/check.py
Token counts
Every dataset states the same heuristic in its dataset.json:
approx_tokens = ceil(sum(len(m.content) for m in messages) / 4)
It is tokenizer-independent on purpose, so a prompt's bucket does not change when
you swap models. Real counts differ: for Latin-script prose the heuristic is
usually within ±15 %, for source code it under-counts a little, and for Chinese
and Japanese the real count runs roughly 2–4× higher. Workloads name a nominal
input_tokens; results should record what the engine actually reported in
metrics.input_tokens_total.
Row schemas
kind: prompts
{
"id": "mix-0001",
"topic": "science", // one of the dataset's topics
"bucket": "m", // xs | s | m | l | xl | xxl, see below
"lang": "en", // natural language of the prompt
"approx_tokens": 597, // chars/4, includes shared_prefix when set
"messages": [{ "role": "user", "content": "..." }],
"shared_prefix": null, // string | null
}
Length buckets are inclusive ranges on approx_tokens:
| bucket | tokens | typical content |
|---|---|---|
xs |
16–64 | a single question |
s |
65–256 | a short context plus a task |
m |
257–1024 | a page of a document plus a task |
l |
1025–4096 | a long report, source file or transcript |
xl |
4097–16384 | a big document |
xxl |
16385–65536 | a very big document |
shared_prefix is a contract: when it is non-null the harness must send it as
a leading system message, [{role: "system", content: shared_prefix}, *messages].
approx_tokens already includes it. Only prompts-shared-prefix-v1 uses it.
prompts-code-v1 adds code_lang (python | typescript | go | rust |
sql) and task (write | fix | explain | refactor), and its rows have a
system message naming the language.
kind: eval
{
"id": "math-0001",
"category": "arithmetic",
"difficulty": "easy", // easy | medium | hard
"prompt": "...", // or "messages": [...] when a system turn matters
"answer": "42", // string | number | object, per scorer
"scorer": "numeric",
"choices": ["...", "..."], // mc rows only; answer is the letter label
"tests": "assert f(1) == 2", // code_exec rows only
"image": "images/vis-0001.png", // vision rows only, relative to the dataset dir
"meta": {}, // per-scorer extras, see below
}
Always use the row's own scorer. A workload's eval.scorer is only the
dataset default; eval-reasoning-v1 mixes mc and exact,
eval-multilingual-v1 mixes contains, mc and numeric, and the -v2
evals mix mc, exact and numeric per row.
kind: haystack
{
"id": "hay-8k-d50",
"kind": "single", // single | multi
"target_tokens": 8192,
"seed": 7008292,
"algorithm": "haystack-v1",
"needles": [{ "depth": 0.5, "text": "...", "answer": "48213", "line_number": 143 }],
"question": "...",
"answer": "48213",
"chars": 32771,
"lines": 331,
"approx_tokens": 8193,
"sha256": "…", // digest of the materialised document
"static_file": "static/hay-8k-d50.txt", // null above 32k tokens
}
Answer extraction and scorers
Applied in this order to the raw model output, before any scorer except
instruction (which needs the raw text) runs:
- drop everything inside
<think>…</think>, including an unterminated leading<think>block; - drop markdown code fences, keeping the fenced content;
- if any line matches
/^\s*(?:final answer|answer)\s*[:\-]\s*(.+)$/i, take the capture of the last such line and use only that; - strip surrounding whitespace, matching quotes, and a single trailing
.or!.
| scorer | answer |
rule |
|---|---|---|
exact |
string | case-insensitive after collapsing whitespace; meta.answer_aliases also accepted |
numeric |
numeric string | parse the last number in the output (thousands separators, leading currency symbol and trailing % stripped); correct within max(meta.tolerance or 1e-6, 1e-9· | expected | ) |
mc |
"A"…"D" |
accept the bare letter, A), (A), A. or the full text of the correct choice |
contains |
{all: [...], any: [...]} |
casefolded substring match, no diacritic folding. An entry may be a list of alternatives that passes when any one is found |
json |
expected value | parse the output as JSON; meta.match is subset (default) or exact; arrays compare elementwise in order; numbers compare numerically |
code_exec |
reference solution (unused) | run extracted_code + "\n\n" + tests in a subprocess, no network, throwaway cwd, meta.timeout_s (default 10) |
needle |
string | casefolded substring test after removing spaces, commas and hyphens from both sides |
instruction |
rule set | evaluate the DSL below against the raw output |
vision |
– | attach row.image as a base64 data URL image part next to the prompt, then apply the row's own scorer |
integrity |
"clean" |
long-output token integrity, not correctness: mask strings, comments and regex literals, then flag a digit-initial token that is not a valid numeric literal (128Pin), an undefined identifier that is a defined name plus 2–6 lower-case letters (carrier+hed), or an undefined bare word between two numeric literals ([6, visible, 0]). The definition set is meta.context_identifiers ∪ what the output declares ∪ the JS globals. An ordinary undefined identifier is not counted |
One item is correct or it is not; there is no partial credit.
accuracy = correct / total. A request that failed (timeout, 5xx,
context overflow) counts as incorrect and appears in scores.failures and
metrics.requests_failed. Output that is merely malformed — invalid JSON, a
refusal, a preamble where one word was asked for — is a wrong answer, not a
failed request.
eval-tools-v1
meta.tools holds OpenAI function schemas; send them as the request's tools
with tool_choice: "auto" and score tool_calls[0]:
answer.tool_call = {name, arguments}→ the name must match and the parsed arguments must match, subset by default (extra optional arguments tolerated) unlessmeta.arguments_matchis"exact". Strings compare case-insensitively after stripping; numbers compare numerically.answer.tool_call = null→ correct only when the response contains no tool call at all. The text of the reply is not scored.
eval-longgen-integrity-v1
Rows carry a 20–25k-character synthetic JavaScript ES-module project as the prompt plus a
task: write one complete new module of the stated line range, importing at least six named
exports from the project, and reply with the file contents and nothing else. answer is
"clean" — the expected observation is "no spliced token", not a string the model types —
and difficulty orders the items by requested output length (easy ≈ 1200 output tokens,
medium ≈ 1800, hard ≈ 2400, recorded exactly in meta.target_output_tokens).
meta.context_identifiers is every identifier the generated project defines or exports,
computed from the generated source. The scorer needs it: without a definition set it cannot
tell a spliced name from one the project supplied. The generic Answer:-line extraction
step is not applied here — the output is a source file, and a line beginning answer:
inside it is code.
An item is correct when the generation contains no splice. That is deliberately not a correctness check: wrong-but-intact code scores 1.0, and the other eval suites are where correctness is measured. A clean run of 36 is also not proof of absence — at an observed rate of roughly one affected generation in five, a clean pass is a plausible outcome for an affected build.
eval-vision-v1
meta.ground_truth records what was drawn (shape list, bar heights, clock hands,
dice values) for debugging a disagreement; it is not used for scoring. A text-only
model scores near zero — record that as a real result, because some engines
silently drop image parts instead of failing.
The instruction rule DSL
eval-instruction-v1 rows carry answer = {"all": [rule, …]}. The item is correct
when every rule passes against the raw output. The normative implementation is
eval-instruction-v1/rules.py —
evaluate(answer, text) -> (bool, failed_rules) — and
python datasets/eval-instruction-v1/rules.py --self-test must pass. The harness
instruction scorer must agree with it; check.py runs the self-test.
Shared definitions, all normative:
- word — a match of
[A-Za-z0-9]+(?:['’-][A-Za-z0-9]+)*;don'tis one word,3.5is two - lines —
text.strip().split("\n")with blank lines dropped - paragraphs —
re.split(r"\n\s*\n", text.strip()), empties dropped - sentences — split on
[.!?]+followed by whitespace or end of text - bullets — lines whose
lstrip()starts with a marker followed by a space
| rule | parameters | passes when |
|---|---|---|
word_count |
min?, max? |
the word count is in range |
char_count |
min?, max? |
len(text.strip()) is in range |
sentence_count |
min?, max? |
the sentence count is in range |
line_count |
min?, max? |
the non-empty line count is in range |
paragraph_count |
min?, max? |
the paragraph count is in range |
bullet_count |
min?, max?, markers?, only_bullets? |
the bullet count is in range; with only_bullets every line must be a bullet |
numbered_list |
count |
line-leading numbers are exactly 1..count in order |
contains_all |
values[], case_sensitive? |
every value occurs (an entry may be a list of alternatives) |
contains_none |
values[], case_sensitive? |
no value occurs |
contains_any |
values[], min_matches?, case_sensitive? |
at least min_matches values occur |
starts_with |
value, case_sensitive? |
text.strip() starts with it |
ends_with |
value, case_sensitive? |
text.strip() ends with it |
all_caps |
– | at least one letter, every cased letter upper case |
all_lower |
– | at least one letter, every cased letter lower case |
no_commas |
– | no , anywhere |
regex |
pattern, flags? (ims), mode? |
search, or fullmatch against text.strip() |
not_regex |
pattern, flags? |
the pattern is not found |
json_only |
– | the whole output parses as JSON once a surrounding fence is removed |
json_path_equals |
path, value |
the dotted path (numeric segments index arrays) equals the value |
is_number |
– | text.strip() with , removed parses as a float |
word_repeat |
value, min?, max?, case_sensitive? |
the word occurs that many times |
max_words_per_line |
max |
no line exceeds it |
every_line_starts_with |
value |
every non-empty line starts with it after lstrip() |
unique_lines |
– | no two non-empty lines are identical |
Each row also carries meta.example_pass, a compliant answer written by the
generator and verified against the rules — proof that the rule set is satisfiable.
It is never sent to the model.
The haystack algorithm
haystack-v1 stores recipes, not text: a 256k-token document is a megabyte,
and there are eight sizes. The harness rebuilds the exact document from the recipe
using haystack-v1/build.py, which is standard-library
only so bench/ can import it directly. Given seed, target_tokens and
needles:
budget = target_tokens * 4(the same chars/4 heuristic);rng = random.Random(seed);needle_chars = Σ(len(text) + 13 + 1)over the needles, where 13 islen("Line 000000: ");- generate filler sentences one at a time —
rng.choice(TEMPLATES), then fill its slots left to right, onerngcall each — stopping as soon asfiller_chars + needle_chars >= budget, counting each line as13 + len(sentence) + 1; - insert the needles sorted by depth ascending: needle
j(0-based) goes to indexround(depth_j * F) + j, whereFis the number of filler lines, clamped to[0, len(lines)]; - number every line from 1:
f"Line {i:06d}: {sentence}"; - join with
"\n", no trailing newline.
Steps 4 and 5 must stay in that order or the numbering changes. Two
implementations agree exactly when they produce the same SHA-256, which every row
records in sha256 and check.py re-verifies. build_prompt(item) returns
PREAMBLE + "\n\n" + document + "\n\n" + question, which is what a longctx eval
must send.
Documents at or below 32k tokens are also materialised as
haystack-v1/static/<id>.txt for tooling that wants a file; those carry one
trailing newline that is not part of the hashed text.
eval-longctx-v1 embeds its own recipe per row in meta.haystack, so it is
self-contained; its prompt is the question only, and sending it without the
document is a bug that reads as a catastrophic accuracy drop.
Adding a dataset
- Write one generator in
_gen/(PEP 723 header, fixed seed, stdlib or Pillow), and make it prove its own answers — compute them, brute-force them, or execute them. Do not hand-type a key. - Emit
dataset.jsonvia_lib.base_dataset_json/_lib.eval_dataset_jsonso the licence, provenance, token heuristic and scoring block stay identical across datasets. - Run
uv run datasets/_gen/check.pyuntil it is green, and run the generator twice to confirm the output is byte-identical. - Add a row to the index above and to
_gen/README.md, and add the workload that references it (workloads/README.md). - Keep the whole of
datasets/under 25 MB. If a dataset would be large, store a recipe and a reference builder, the wayhaystack-v1does.
Content rules: written or generated here, never copied; unique text rather than repeated text, so prefix caching and tokenizer merges cannot flatter a prefill number; facts that cannot go stale; and no item whose answer depends on taste, sentiment or the current date.