Skip to main content

Robust, resumable LLM dataset annotation

CI codecov PyPI version Python versions License

llm-annotator is a Python 3.12+ library for robust, resumable LLM-driven dataset annotation and generation.

It supports multiple providers through pluggable clients:

  • vLLM offline inference (in-process): VLLMOfflineClient
  • vLLM online inference (server API): VLLMOnlineClient
  • OpenAI API: OpenAIClient
  • Anthropic API: ClaudeClient

Key capabilities:

  • No-code config runs: describe prompts, schemas, model, dataset and multiple chained annotation steps in one JSON/YAML file and run it with llm-annotate my-pipeline.yaml.
  • Staged pipeline: prepare_data + run_annotation separates expensive template application and sorting from model inference, enabling SLURM and cluster restart workflows.
  • Multi-server vLLM: VLLMQueueAnnotator runs one workload over a pool of vLLM servers (e.g. one per GPU of a multi-node allocation); see examples/vllm-server-pool/ for a config-driven and a Python-API example.
  • SLURM out of the box: slurm/submit_pipeline.sh turns a config into one job chain per step, with everything cluster-specific in a single cluster file; see slurm/README.md.
  • Resumable processing with JSONL checkpoints.
  • Annotation of existing datasets and generation from scratch.
  • Structured outputs via JSON schema.
  • Reasoning traces in their own column, for thinking models on either vLLM provider or on Claude.
  • Retry and validation hooks for robust pipelines.
  • Optional Hugging Face Hub upload cadence for both prepared data and outputs.
  • Context-manager cleanup of client resources.

It is not intended for parallel, multi-node, multi-instance generation. If that is what you are after, maybe datatrove is something for you.

Documentation

Read the full documentation at bramvanroy.github.io/llm-annotator.

Provider setup reference: docs/provider-info.md

Installation

Recommended:

uv add llm-annotator

or

pip install llm-annotator

Install provider extras as needed:

uv add "llm-annotator[vllm]"
uv add "llm-annotator[openai]"
uv add "llm-annotator[anthropic]"

See docs/provider-info.md for auth environment variables and provider-specific setup notes.

Prebuilt vLLM kernels

In the context of SLURM it may be advisable to have the vLLM kernels prebuilt so that time is not wasted for JIT-compilation, no storage contention in the case of multiprocessing, etc. Installing the kernels up front avoids both, which matters most when you serve models on a cluster.

These kernels cannot be shipped as an extra of llm-annotator: flashinfer-jit-cache is not on PyPI at all (it is published per CUDA version on FlashInfer's own index) and the flashinfer-cubin on PyPI trails the releases vLLM pins against. An extra would therefore fail to resolve for anyone installing llm-annotator from PyPI. Install them next to the vllm extra instead, matching the flashinfer-python version vLLM pulled in and the CUDA version your torch wheel was built against:

version=$(python -c "import importlib.metadata as m; print(m.version('flashinfer-python'))")
cuda=cu$(python -c "import torch; print(torch.version.cuda.replace('.', ''))")

uv pip install "flashinfer-cubin==$version" --index-url https://flashinfer.ai/whl/
uv pip install "flashinfer-jit-cache==$version" --index-url "https://flashinfer.ai/whl/$cuda/"

Use pip install instead of uv pip install if you installed with pip. The CUDA version comes from torch.version.cuda.

Usage

One-step convenience

Annotate an existing dataset:

from llm_annotator import Annotator, VLLMOfflineClient

client = VLLMOfflineClient(
    model="meta-llama/Llama-3.2-3B-Instruct",
    max_model_len=4096,
)

with Annotator(client=client, verbose=True) as anno:
    ds = anno.annotate_dataset(
        output_dir="outputs/sentiment",
        prompt_template="Classify the sentiment of this text: {text}",
        dataset_name="stanfordnlp/imdb",
        dataset_split="test",
        max_num_samples=100,
    )

Generate a dataset from scratch:

from llm_annotator import Annotator, OpenAIClient

client = OpenAIClient(model="gpt-4o-mini")

with Annotator(client=client) as anno:
    ds = anno.generate_dataset(
        output_dir="outputs/generated-qa",
        prompts="Write a short geography quiz question with answer.",
        max_num_samples=200,
    )

Two-step staged workflow

For large datasets or cluster (SLURM) environments, split the pipeline explicitly into a preparation step and a generation step. prepare_data applies prompt templates, optional sorting, and saves the prepared artifacts locally and to Hugging Face Hub. run_annotation then handles only model inference. If generation fails, re-run it with the same output_dir and hub_id: the prepared data is restored and the samples already recorded in the progress files are skipped.

A single hub_id drives every Hub destination: the prepared data and the JSONL progress backup live on temporary branches of that repo, the final dataset is pushed to its main branch, and both temporary branches are deleted once the run completes.

from llm_annotator import Annotator, VLLMOfflineClient

client = VLLMOfflineClient(
    model="meta-llama/Llama-3.2-3B-Instruct",
    max_model_len=4096,
)

HUB_ID = "my-org/imdb-sentiment"  # backups *and* the final dataset

with Annotator(client=client, verbose=True) as anno:
    # Step 1: prepare data (reuses local cache or Hub backup if available)
    prepared_dataset, local_path, hub_id = anno.prepare_data(
        output_dir="outputs/imdb-sentiment",
        prompt_template="Classify the sentiment of this text: {text}",
        dataset_name="stanfordnlp/imdb",
        dataset_split="test",
        max_num_samples=100,
        sort_by_length=True,
        hub_id=HUB_ID,
    )

    # Step 2: run generation against the prepared data
    ds = anno.run_annotation(
        output_dir="outputs/imdb-sentiment",
        prompt_template="Classify the sentiment of this text: {text}",
        prepared_dataset=prepared_dataset,
        hub_id=HUB_ID,
        upload_every_n_samples=500,
    )

To force a fresh preparation (ignoring any cached or Hub-stored artifacts), pass force_data_preparation=True to prepare_data or to annotate_dataset.

Run from a config file

The same work can be described in a single JSON or YAML file and run without writing any Python:

llm-annotate my-pipeline.yaml
# or, from a checkout: python scripts/annotate.py my-pipeline.yaml

A config lists one or more steps that run in order, each annotating the dataset the previous one produced. That is what makes generate-then-judge workflows possible: one model writes question-answer pairs, a second rates them.

output_dir: outputs/pipeline-qa

dataset:
  name: stanfordnlp/imdb
  split: test
  max_num_samples: 20

client:
  provider: vllm_offline
  model: Qwen/Qwen3-8B
  options:
    max_completion_tokens: 512

steps:
  - name: write-qa
    prompt_file: prompts/write_qa.md
    output_schema_file: schemas/qa.json    # produces `question`, `answer`
    filter_invalid: true
    rename:
      question: question_v1

  - name: rate-qa
    prompt: "Rate this question about the text.\n\n{text}\n\nQ: {question_v1}"
    output_schema_file: schemas/rating.json
    client:
      provider: claude                     # a different judge
      model: claude-haiku-4-5

Paths inside the config resolve relative to the config file, so a config directory is self-contained. Finished steps write a snapshot and are skipped on a re-run, so an interrupted pipeline resumes rather than starting over.

A complete, runnable example lives in examples/pipeline-qa/, and the full key reference is in docs/pipeline.md.

Run it on SLURM

The same config runs on a cluster without a scheduler-specific rewrite. Fill in one small cluster file (partitions, accounting, cores per GPU) and submit:

cp slurm/cluster.env.example slurm/cluster.env
./slurm/submit_pipeline.sh --dry-run my-pipeline.yaml   # inspect the jobs
./slurm/submit_pipeline.sh my-pipeline.yaml

Each step becomes its own job chain: a step served by vLLM gets a GPU server array plus a client, a step on a hosted API gets a CPU-only job, and GPUs are released as soon as the step that needed them is done. Details in slurm/README.md.

See the documentation for more examples, including:

  • Structured output with JSON schemas
  • Custom validation and post-processing
  • Generating datasets from scratch

Or check out the examples/ directory for complete working examples.

Testing

Install development dependencies first:

uv sync --dev

Run the default checks:

make style
make quality
make test
make typecheck

Pytest marker targets:

# Fast tests (same as `make test`)
make test-fast

# Slow tests only
make test-slow

# Integration tests only
make test-integration

# Entire suite (fast + slow)
make test-all

You can also run markers directly with pytest:

uv run pytest -m "not slow"
uv run pytest -m "slow"
uv run pytest -m "integration"

Slow and integration tests may load local models, require more runtime, or depend on optional components.

Building documentation

Local versioned docs preview (uses mike on a temporary local branch):

make serve-docs

Override version metadata when needed:

make serve-docs DOCS_VERSION=0.4.0 DOCS_ALIAS=latest DOCS_SOURCE_REF=v0.4.0

Docs are published with mike on release tags through .github/workflows/docs.yml.

Metadata

Release files for llm-annotator 0.15.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for llm-annotator 0.15.0
File Size Uploaded
llm_annotator-0.15.0.tar.gz 607.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for llm-annotator 0.15.0
File Interpreter ABI Platform
llm_annotator-0.15.0-py3-none-any.whl Python 3 none any Details

Total release size: 729.8 kB

Release files / llm_annotator-0.15.0.tar.gz

Download URL llm_annotator-0.15.0.tar.gz
Size 607.2 kB
Tags Source
SHA-256 checksum
How to use checksums
77aa7eb1bb6b6a844285c2cf37446eda2efc95dbee7e6d66eaeec3c3152d9bb7
BLAKE2b-256 checksum
How to use checksums
e8bb1125936afe99895656ad2bdcc1b6f061f7b3416da82ed37df6a08089eaaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / llm_annotator-0.15.0-py3-none-any.whl

Download URL llm_annotator-0.15.0-py3-none-any.whl
Size 122.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e5fbaa9f02dff8b590081bbcd96ee0177016402ae4e090c2be694dbab88e6336
BLAKE2b-256 checksum
How to use checksums
cf0081970baf49937b2014ce3bb3ca94b93c06b6ef887d8de52c7e0af66aa384
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

This release

0.15.0 This release

2 release files

0.13.2

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.2

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.4.0

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page