llmexer is a CLI tool for LLM experiments. Use it to build datasets (publications, metadata, prompts) and to design, run and evaluate experiments on them.
πͺ The idea behind the tool:
everythingis afile. Projects, experiments, searches and configs are all stored as files. The CLI edits them for you, but you can also edit them by hand β add a model, change a search, drop in a PDF, or open the generated SQLite database and inspect it yourself.
The package is in early beta. Breaking changes may arrive at short notice.
Install with pip:
pip install --upgrade llmexerOr with uv:
uv pip install --upgrade llmexerThen set up your configuration, as described below.
llmexer needs access to a local or remote LLM. It reads your credentials and your current project from a .env file.
Create a .env file in the project root and add the settings you need. All of them are optional.
Current project
PROJECT_ID=20260330-3a9adf70LLM providers β used by experiment run and experiment try:
# Base URL of a provider
PROVIDER_OLLAMA_URL=http://localhost:11434/v1
PROVIDER_VLLM_URL=http://localhost:8000/v1
# API key of a provider
PROVIDER_OPENAI_KEY=sk-...Name each variable PROVIDER_<PROVIDER>_URL or PROVIDER_<PROVIDER>_KEY, where <PROVIDER> is the provider name in uppercase: OLLAMA, VLLM, LITELLM, OPENAI or GEMINI.
The litellm provider talks to a LiteLLM proxy. It has no default URL and always needs a token, so set both. If either is missing, experiment run stops straight away with a clear message instead of failing later with an opaque 401:
PROVIDER_LITELLM_URL=https://your-litellm-proxy.example.org/v1
PROVIDER_LITELLM_KEY=sk-...Literature search β search run queries Semantic Scholar first, then OpenAlex if you set a key:
OPENALEX_API_KEY=...
# Cap on OpenAlex results per query (default 5000)
MAX_OPEN_ALEX_RESPONSES=5000
# Used for DOI downloads via Unpaywall, and as the OpenAlex polite-pool address
UNPAYWALL_EMAIL=you@example.comPDF text extraction β used by papers extract --processor docling:
DOCLING_URL=http://localhost:5001/
DOCLING_USER=myuser
DOCLING_PASSWORD=mypasswordWhere files are stored β by default the CLI writes into the current directory:
LLMEXER_BASEDIR=my-projectsTo use a different set of variables for one run, pass your own file:
llmexer --env-file custom.envThis is the usual path from an empty project to analysed results.
1. Create a project
llmexer project create2. Give it a meaningful name
llmexer project rename --old-id 20260402-a1b2c3d4 --new-id llm-survey-20263. Set up the project structure
This creates an experiment/ folder with template CSVs and a starter prompt:
llmexer experiment init --pid llm-survey-2026| File | What it holds |
|---|---|
experiment/llms-for-experiment.csv |
The models to test. One row is one model run under one profile, so list a model twice to run it under two profiles. |
experiment/data.csv |
Your input rows. |
experiment/mapping.csv |
Which prompt each data row uses. |
experiment/llm-params.csv |
Hyperparameter profiles, keyed by provider, model and profile name. |
experiment/prompts/prompt01.txt |
A starter Jinja2 prompt template using {{title}} and {{abstract}}. |
4. Build the experiment database
First fill in the CSVs and write your prompt templates. Then pair the data rows with the prompts you want:
llmexer experiment map --pid llm-survey-2026 --prompt prompt01,prompt02map is optional, and most useful after experiment copy-papers or experiment copy-search has replaced your data.csv. It pairs every data row with every prompt you select and backs up the old mapping.csv.
Now generate the experiments:
llmexer experiment generate --pid llm-survey-2026generate renders every combination of data row, prompt, model and parameter profile, then writes them to a SQLite database in experiment/. Models and profiles are matched on provider, model name and profile name. Each of those combinations must be unique in each CSV; if one is repeated, generate reports it and stops. A model with no matching profile is reported and skipped.
Add --dry-run to see how many rows you would get, without writing anything:
llmexer --dry-run experiment generate --pid llm-survey-2026π‘ Hint: the experiment store is a plain SQLite database (
experiment/experiment_*.db), so you can also open and edit it in a tool such as DBeaver. Each provider has anexperiment_<provider>table for the rows and aparams_<provider>table for the hyperparameters. Join them to see each row next to the parameters it ran with:SELECT e.*, p.* FROM experiment_ollama e JOIN params_ollama p ON e.params_code = p.params_code AND e.profile_name = p.profile_name;
5. Add later combinations to the database (optional)
If you add a model, a profile or a few mapping rows after generating, update appends what is missing instead of starting a new database:
llmexer experiment update --pid llm-survey-2026Stored rows and the results already collected for them are never touched.
Hyperparameters are never rewritten either. If a profile keeps its name but has different values, update reports the differences and stops β otherwise you could not tell rows generated before the change from rows generated after it. Give the changed parameters a new profile name, point the model row at it, and run update again. A changed prompt or data cell is only reported as a warning, and the stored rows keep the text they were generated with.
6. Try one combination (optional)
Before running everything, check what one prompt returns for one data row under one profile:
llmexer experiment try --pid llm-survey-2026 \
--prompt prompt01 --profile ollama-default --data-id D01try is generate and run for a single combination. It checks all three names first, so a typo stops the command before any LLM call. The model and provider come from the profile; if one profile name covers several models, try lists them and asks you to add --model or --provider. Every try is kept as history in the database, together with the parameters it used, and leaves your generated rows and their results alone. Add --dry-run to print the rendered prompt without calling anything.
7. Run the experiment
Call the LLMs and collect the results:
llmexer experiment run --pid llm-survey-2026 --file experiment_<SAMPLE>.dbResults are written back into the same database, so it stays your single source of truth. Each call is also saved as a JSON file in experiment/responses/. If you run the command again, rows that already finished successfully are skipped.
Scenario 4 covers the rest of the options: previewing a run, narrowing it to one provider, model or profile, running a single row, and running several calls at once.
8. Check the statistics
llmexer experiment stats --pid llm-survey-2026You get totals, token counts, and a breakdown per provider and per model. If the project holds several databases, pass --file to choose one:
llmexer experiment stats --pid llm-survey-2026 --file experiment_<SAMPLE>.db9. Export the results as HTML (optional)
This renders the experiment as one self-contained page, which is handy for screening or sharing:
llmexer experiment export --pid llm-survey-2026 --file experiment_<SAMPLE>.dbThe page has sortable columns, per-column filters and a copy button on every cell. Each row also carries a ready-to-run experiment try command, so you can copy a cell and re-run that single combination in a shell.
10. Analyse the results in a notebook (optional)
llmexer analysis init --pid llm-survey-2026This creates an analysis/ folder with two notebooks and the Python modules they call. Open analyse_experiment.ipynb and run all cells: it loads the experiment database, parses each model answer from JSON into its own columns, exports those answers as a CSV, and prints the statistics and charts. analyse_searches.ipynb does the same for one search.
The modules are copies, so each project can grow its own analysis. The notebooks import only those copies and never llmexer, so the folder stands on its own. To restore the shipped versions, run llmexer analysis init --rewrite; your copies are backed up first.
Running the notebooks needs the optional analysis packages:
uv pip install -e . --group analysisπ‘ The CLI has many more options than this guide shows. Run
llmexer --help, orllmexer <category> <command> --help, to see them all.
From a local file:
llmexer papers add --pid llm-survey-2026 --file ~/Downloads/attention-is-all-you-need.pdfFrom a directory of PDFs:
llmexer papers add --pid llm-survey-2026 --directory ~/Downloads/papers/From a URL:
llmexer papers add --pid llm-survey-2026 --url https://arxiv.org/pdf/1706.037621. Run a search
Create a search configuration, then run it:
llmexer search create --pid llm-survey-2026 --query "large language models"
llmexer search list --pid llm-survey-2026
llmexer search run --pid llm-survey-2026 --file 20260401-bfdd863d.yamlOr run a query directly:
llmexer search run --pid llm-survey-2026 --query "large language models" --limit 500Results are saved as <ID>__results.csv in searches/.
2. Filter out rows you do not need (optional)
filter removes rows and writes <ID>__filtered.csv. Filters chain: each run reads the existing filtered file, or the results file if there is none, and rewrites it. You can combine --language, --source, --doi and --downloaded in one run:
# drop German rows, and rows that are not downloaded yet
llmexer search filter --pid llm-survey-2026 --file 20260401-bfdd863d.yaml --language de --downloadedLeave out --file to apply the same filters to every search in the project. Each applied filter is logged, so you can see later what was removed.
3. Export search results as HTML (optional)
llmexer search export --pid llm-survey-2026 --file 20260401-bfdd863d.yamlThis writes an HTML page next to the CSV, with sortable columns, per-column filters, clickable DOIs and collapsible abstracts. Leave out --file to export every search.
1. Download open-access papers by DOI
llmexer uses the Unpaywall API, which needs your email address:
llmexer papers download --pid llm-survey-2026 --doi 10.1038/nature12373 --email you@example.comYou can also download every paper with a DOI from a search result file:
llmexer papers download --pid llm-survey-2026 --search-file 20260401-bfdd863d__results.csvOr from a filtered file, to download only the papers that passed your filters:
llmexer papers download --pid llm-survey-2026 --search-file 20260401-bfdd863d__filtered.csvFailed downloads are written to a CSV in searches/logs/, so you can retry them. When the download finishes, the search is synced against your papers/ folder: the listed rows are updated, and no new rows are added.
2. Extract text from the papers
The default pypdf backend saves .txt files:
llmexer papers extract --pid llm-survey-2026The docling backend gives richer Markdown output and saves .md files. It reads its connection details from .env:
llmexer papers extract --pid llm-survey-2026 --processor doclingYou can also pass those details directly:
llmexer papers extract --pid llm-survey-2026 --processor docling \
--docling-url http://myserver:5001/ \
--docling-user admin \
--docling-password secretPapers that already have an extracted file are skipped. Use --rewrite to extract them again:
llmexer papers extract --pid llm-survey-2026 --rewrite1. Preview a run without calling any LLM
llmexer --dry-run experiment run --pid llm-survey-20262. Run the rows of one provider
This helps when only one backend is available, such as a local ollama:
llmexer experiment run --pid llm-survey-2026 --filter-provider ollama3. Run one model or one profile
Both names must match in full, and they are case-sensitive:
llmexer experiment run --pid llm-survey-2026 --filter-model gemma4:31bllmexer experiment run --pid llm-survey-2026 --filter-profile ollama-creativeYou can also combine the filters:
llmexer experiment run --pid llm-survey-2026 --filter-provider ollama --filter-model gemma4:31b4. Run a single row by its code
llmexer experiment run --pid llm-survey-2026 \
--file experiment_<SAMPLE>.db --code 15. Run several LLM calls at once
Most of a run is spent waiting for the backend, so --parallel-calls lets several calls run at the same time. The limit applies across all providers, and the default is 1, which runs the rows one after another. Results are always written one at a time.
llmexer experiment run --pid llm-survey-2026 --parallel-calls 4Most commands take a --pid option to say which project to use. If you set PROJECT_ID in your .env file, you can leave it out:
# in .env
PROJECT_ID=my-project
# uses my-project
llmexer search run --query "machine learning"Pass --pid to override it for a single command:
llmexer search run --pid different-project --query "deep learning"A project is the top-level container for your experiments, papers and searches. The project category (alias: proj) manages them.
| Command | What it does | Example |
|---|---|---|
create |
Creates a project folder under .projects/. Takes an optional custom ID. |
llmexer project create --id my-project |
current |
Shows the current project ID from .env. |
llmexer project current |
rename |
Renames a project. Uses PROJECT_ID from .env if you leave out --old-id. |
llmexer project rename --old-id old-name --new-id new-name |
The experiment category (alias: exp) sets up, generates and runs the experiments inside a project.
| Command | What it does | Example |
|---|---|---|
init |
Creates the experiment/ folder with template CSVs and a starter prompt. Stops if the project is already set up. |
llmexer experiment init --pid my-project |
copy-papers |
Copies the extracted text of your papers into data.csv, one row per paper. Backs up the old file first. |
llmexer experiment copy-papers --pid my-project |
copy-search |
Copies a search result CSV into data.csv, keeping the original row order. Backs up the old file first. |
llmexer experiment copy-search --pid my-project --file <SEARCH_ID>__results.csv |
map |
Rebuilds mapping.csv by pairing every data row with the prompts you select. Use --prompt to pick them, or leave it out to use all of them. An unknown prompt name stops the command. Backs up the old file first. Supports --dry-run. |
llmexer experiment map --pid my-project --prompt prompt01,prompt02 |
generate |
Renders every combination of data row, prompt, model and parameter profile into a new SQLite database. A model without a matching profile is skipped, and a repeated combination stops the command before anything is written. Supports --dry-run. |
llmexer experiment generate --pid my-project |
update |
Appends new combinations to an existing database instead of generating a new one. Stored rows and their results stay as they are. A profile whose values changed under the same name stops the command; rename the profile and run it again. Supports --dry-run and --file. |
llmexer experiment update --pid my-project |
try |
Renders and runs one combination of data row, prompt and profile, then prints the answer. All three names are checked first, so a typo stops the command before any LLM call. Each try is kept as history, with the parameters it used. Supports --dry-run. |
llmexer experiment try --pid my-project --prompt prompt01 --profile ollama-default --data-id D01 |
run |
Runs every row of a generated database against its provider and writes the results back into the same database. Rows that already finished are skipped. Narrow the run with --filter-provider, --filter-model, --filter-profile or --code, and raise the throughput with --parallel-calls. Supports --dry-run and --file. |
llmexer experiment run --pid my-project --filter-provider ollama |
stats |
Shows the totals, token counts, and a breakdown per provider and per model. The same model served by two providers is reported once per provider. Pass --file to pick a database. |
llmexer experiment stats --pid my-project |
list |
Lists all projects with their setup state and their experiment databases. Sort with --sort-by and --desc. |
llmexer experiment list --sort-by date --desc |
export |
Renders an experiment database as an HTML page next to it, with sortable columns, per-column filters and a ready-to-run try command per row. Supports --file, --rewrite and --dry-run. |
llmexer experiment export --pid my-project |
The analysis category (aliases: analyse, analyze) sets up a Jupyter workspace for a project. It creates an analysis/ folder next to experiment/, papers/ and searches/, holding two notebooks and the Python modules they call.
The notebooks hold no logic of their own β the cells set the paths, call functions and show the result β so the analysis itself stays testable code. The modules are copies, and the notebooks import only them, never llmexer. That keeps the folder self-contained and lets you adapt it per project.
| Command | What it does | Example |
|---|---|---|
init |
Creates <project>/analysis/ with analyse_experiment.ipynb, analyse_searches.ipynb and the modules they call. The experiment notebook loads the database, parses each answer from JSON into columns, exports them as a CSV, and prints the statistics and charts. The searches notebook breaks one search down by engine, year, open access and language. Both work on an empty project. Existing files are only replaced with --rewrite, which backs them up first. Supports --dry-run. |
llmexer analysis init --pid my-project |
The notebooks read their database and search files from literals written in when the folder is created, so you can edit them by hand. They do not refresh themselves: after another experiment generate or search run, change the value or run init --rewrite. Searches are analysed one at a time, because two queries describe two different populations.
Running the notebooks needs the optional analysis packages: uv pip install -e . --group analysis.
The papers category manages the PDFs of a project.
| Command | What it does | Example |
|---|---|---|
add --file |
Copies one PDF into the project's papers/ folder. |
llmexer papers add --file /path/to/paper.pdf |
add --directory |
Copies every PDF from a directory and its subdirectories. Papers already in the project are skipped. | llmexer papers add --directory /path/to/folder |
add --url |
Downloads a PDF from a URL. | llmexer papers add --url https://example.com/paper.pdf |
download --doi |
Downloads open-access PDFs by DOI through the Unpaywall API. Needs your email, from --email or UNPAYWALL_EMAIL. |
llmexer papers download --doi 10.1038/nature12373 --email you@example.com |
download --search-file |
Downloads every paper with a DOI from a search result or filtered CSV, then syncs the search against papers/. Failures are logged so you can retry them. |
llmexer papers download --search-file 20260401-abc123__filtered.csv |
extract |
Extracts the text of every PDF in papers/. The default pypdf backend saves .txt; the docling backend saves .md. Already extracted files are skipped unless you pass --rewrite. |
llmexer papers extract --pid my-project --processor docling |
The search category manages and runs literature searches. search run queries the Semantic Scholar bulk API. If you set OPENALEX_API_KEY, it then queries the OpenAlex Works API as a second engine and keeps only the publications Semantic Scholar did not already return, matched by DOI or by title. Without the key, OpenAlex is skipped.
| Command | What it does | Example |
|---|---|---|
create |
Creates a search configuration file in the project's searches/ folder. |
llmexer search create --query "machine learning" |
list |
Lists the search configurations of a project as a table. | llmexer search list --pid my-project |
run --query |
Runs a search from a query string and saves the results as a CSV in searches/. |
llmexer search run --query "neural networks" --limit 200 |
run --file |
Runs a search from a configuration file. Use --rewrite to replace existing result files. |
llmexer search run --file 20260401-abc123.yaml |
rename |
Renames a search and all the files that belong to it. | llmexer search rename --old-id 20260401-abc123 --new-id my-search |
stats |
Shows the statistics of a search: papers per year, plus a breakdown by open access, language, download state and source. | llmexer search stats --file 20260401-abc123.yaml |
filter |
Removes rows from a search and rewrites its filtered CSV. Filters chain, and you can combine --language, --source, --doi and --downloaded. Leave out --file to filter every search. Each applied filter is logged. |
llmexer search filter --file 20260401-abc123.yaml --language de --downloaded |
merge |
Merges the project's search CSVs into one deduplicated results file and one deduplicated filtered file, marking which search each row came from and how often it was found. Supports --rewrite and --dry-run. |
llmexer search merge --pid my-project |
sync |
Reconciles a search against the project's papers/ folder and updates which files each row has. Pass --add-local-extra-pdfs to also list PDFs that no row mentions yet. Leave out --file to sync every search. Supports --dry-run. |
llmexer search sync --file 20260401-abc123.yaml |
export |
Renders search results as an HTML page next to the CSV, with sortable columns, per-column filters, clickable DOIs and collapsible abstracts. Leave out --file and --csv-file to export the whole project. Supports --rewrite and --dry-run. |
llmexer search export --file 20260401-abc123.yaml |
To write more precise queries, see the API documentation of both engines:
- Semantic Scholar: Paper bulk search
- OpenAlex: Works
The self category reports on the CLI itself.
| Command | What it does | Example |
|---|---|---|
version |
Prints the installed version. | llmexer self version |
envs |
Lists the environment variables llmexer uses, with passwords masked. |
llmexer self envs |
Before you add papers to a project, you can rename them by their bibliographic metadata β year, journal, authors and title β with the external pdf-renamer tool. You do not need to install it; run it with uvx.
Rename as year, authors, title:
uvx --from pdf-renamer pdfrenamer -f "{YYYY}_{A3etal}_{T}" /path/to/pdfsInclude subdirectories:
uvx --from pdf-renamer pdfrenamer /path/to/pdfs -sfYou can also extract the BibTeX entry of a publication:
uvx --from pdf2bib pdf2bib -s bibtex.bib /path/to/pdfsAn overview of the CLI:
llmexer --helpThe statistics of a search, shown in the terminal:
llmexer search stats --file <filename>Your projects, with the current one highlighted:
llmexer experiment listSearch results exported as a static HTML page, which makes screening easier:
llmexer search exportTo work on llmexer itself, set it up locally with uv.
- Create a virtual environment:
uv venv
- Activate it on macOS or Linux:
Or on Windows:
source .venv/bin/activatecall .venv/Scripts/activate.bat
- Install the package in editable mode with the
devdependencies. Editable mode (-e) is the key to development: it links thellmexercommand to your source code.Theuv pip install -e . --group devdevgroup also pulls in theanalysisgroup (matplotlib, seaborn, ipykernel, jupyterlab), so the notebooks run and their tests import cleanly. To install only what you need to run the notebooks:uv pip install -e . --group analysis



