AI-powered assertion testing for LLM apps — a test runner for model behaviour.
Plune runs an assertion suite against an LLM provider and gives you a pass/fail report —
locally, in CI, or as a regression diff between two runs. You describe the checks in one
plune.yaml; Plune calls the model, evaluates each assertion, caches results, and reports
token cost. Ten built-in assertion types cover plain text, JSON-schema, LLM-as-judge, and
RAG metrics (faithfulness, answer-relevance, context-precision).
Links: plune.ai | npm | GitHub Action on Marketplace
npm install -g @plune-ai/cli # or: pnpm add -g @plune-ai/cli
plune --versionOr run it without installing:
npx -y @plune-ai/cli runRequires Node.js ≥ 20.
# 1. Scaffold plune.yaml, an example dataset, and .env.example
plune init
# 2. Add your provider key (read from the environment / .env — never written to disk)
echo 'ANTHROPIC_API_KEY=sk-ant-...' >> .env
# 3. Run the assertions
plune run
# → 1/1 passed · 0 failed · 0 errored · $0.0008
# 4. Re-render the last run, or diff two runs to catch regressions
plune report --format markdown
plune diff baseline.json current.json --fail-on-regressionEach run writes its full result to .plune/last-run.json.
Plune reads a single plune.yaml, discovered by walking up from the working directory (or
passed with -c <path>). A minimal example:
version: 1
provider:
type: anthropic # anthropic | openai | openrouter
model: claude-3-5-sonnet-latest
evals:
- id: example
prompt: "Answer concisely. {{question}}" # {{vars}} come from each dataset row
dataset: datasets/example.jsonl # a file path, or an inline `examples:` list
assertions:
- type: contains
value: "Paris"Datasets are JSONL, one row per line, shaped { "vars": { ... }, "expected"?: "..." }. The
provider API key is read from the environment based on provider.type:
| Provider | provider.type |
Environment variable |
|---|---|---|
| Anthropic | anthropic |
ANTHROPIC_API_KEY |
| OpenAI | openai |
OPENAI_API_KEY |
| OpenRouter | openrouter |
OPENROUTER_API_KEY |
| Type | Passes when… |
|---|---|
exact-match |
output equals value (optional trim, ignore_case) |
contains |
output contains value |
contains-any |
output contains at least one of values |
contains-all |
output contains every one of values |
json-schema |
output validates against the JSON schema |
llm-judge |
an LLM grades the output against criteria (≥ pass_threshold) |
semantic-similarity |
embedding similarity to reference ≥ threshold |
faithfulness |
output is grounded in context (RAG) |
answer-relevance |
output actually answers the question (RAG) |
context-precision |
context is relevant to the question (RAG) |
| Command | Summary |
|---|---|
plune run |
Run the suite. Flags: --dry-run, --only <id|tag> (repeatable), --bail, --no-cache, --concurrency <n>, --format console|json|markdown, -o, --output <file>. |
plune report |
Re-render the most recent run. Flags: --format, -o. |
plune diff <baseline> <current> |
Compare two plune run --format json outputs and report pass→fail regressions. Flags: --fail-on-regression, --format, -o. |
plune init |
Scaffold plune.yaml, a sample dataset, and .env.example. Flags: --yes (non-interactive), --force. |
plune login |
Save a Plune platform API token so sync and ingest can reach it. The token is checked against the API before it is saved, so a wrong one fails here rather than two commands later. Get one at https://beta.plune.ai → Settings → API tokens. Flags: --token <token> (omit to paste it or pipe it via stdin), --skip-verify (save without checking, for offline setup). |
plune logout |
Remove the saved token. |
plune sync |
Upload the latest local run to the platform. Flags: --file <path> to send a specific run JSON. |
plune ingest [dir] |
Record a Cairn run in Plune. Omit [dir] for the newest run under ./runs, or name the directory holding report.json. Generated cases arrive as review proposals — nothing is created until a person approves it. |
Global flags: -c, --config <path> · -v, --verbose · --no-color.
Exit codes: 0 everything passed · 1 an assertion failed · 2 configuration or execution error.
Everything above works with no account, no network, and no token — that does not change. If you also want run history, trends, and a shared dashboard, the three cloud commands push your local runs to the Plune platform:
plune login # paste the API token from your platform settings page
plune run # exactly as before — the run is saved locally
plune sync # upload .plune/last-run.json, print the read-back URLThe token is stored at ~/.config/plune/credentials.json (mode 0600, honours XDG_CONFIG_HOME)
and is never printed or logged. PLUNE_API_URL points sync at a different server — useful for a
self-hosted backend.
The same engine that powers plune run is exported for use from your own code. Unlike the
CLI, the library does not parse argv or auto-load .env — set the provider key in
process.env yourself.
import { run } from '@plune-ai/cli';
import type { RunResult } from '@plune-ai/cli';
const result: RunResult = await run({ dryRun: false, configPath: 'plune.yaml' });
console.log(result.summary); // { total, passed, failed, errored, ... }Run Plune on every pull request and post a regression diff as a sticky comment with the companion GitHub Action, plune-ai/eval-action:
- uses: plune-ai/eval-action@v1
with:
config: plune.yaml
fail-on-regression: trueBug reports and pull requests are welcome — see CONTRIBUTING.md. For security issues, see SECURITY.md.
MIT © Plune Contributors