speculate
speculate is a lightweight, Docker-ready behavior-driven testing framework for Large Language Models (LLMs).
It helps you write clear, structured specs for how your LLM should behave, then validates actual responses.
Built by Donavan White — powered with Pydantic validation, rich CLI output, and first-class support for Ollama.
✨ Features
- BDD-style scenarios with step chaining: define multiple
.prompt(...).expect_*(...)steps. - System prompt support: global or per-scenario.
- Multi-shot control:
multi_shot(True)(default): steps chain with conversation history per run.multi_shot(False): only the first step runs, no history passed.
- Runs-per-test and accuracy thresholds:
.runs(N)to repeat each scenario..require_accuracy(0.9)to enforce pass rates.
- Seed management:
- Provider-level default seed.
- Scenario override with
.seed(1234). - Randomize seeds per run with
.randomize_seed(True).
- Raw output capture:
.dump_raw(mode="never|fail|always", to_dir="raw_outputs", file_format="txt|json").
- Expectation types:
- Exact match, substring, regex, not equal, not contains, Pydantic schema.
- Rich CLI output with per-run tables, accuracy bars, and suite summary.
- Dockerized runner: reproducible, isolated environment.
- Ollama integration: works with
/api/chatand falls back to/api/generate.
📂 Project Structure
speculate/
├─ __init__.py
├─ core.py # Scenario runner & summary (rich output)
├─ cli.py # CLI entrypoint
├─ providers/
│ ├─ __init__.py
│ └─ ollama_provider.py # Provider wrapper for Ollama API
scenarios/ # Example scenarios
models/ # Example Pydantic models
requirements.txt
Dockerfile
docker-compose.yml
🚀 Quick Start
- Ensure Ollama is running on your host:
ollama serve
ollama run mistral # or another model, e.g. llama3.1, qwen2.5
- Build and run tests in Docker:
docker compose build
docker compose up
Or run directly via the CLI:
python -m speculate.cli scenarios/
After packaging & install, you’ll get:
speculate scenarios/
⚙️ Configuration
Environment variables:
-
OLLAMA_BASE_URL
Default:http://host.docker.internal:11434
Use the Ollama server base URL. -
OLLAMA_API_STYLE
Options:chat→ force/api/chatgenerate→ force/api/generateopenai→ proxy-compatible/v1/chat/completions- (default) auto: try
/api/chat, fall back to/api/generate.
-
OLLAMA_TIMEOUT_S
Timeout in seconds (default 120). -
LLM_DEFAULT_SEED
Provider default seed. -
LLM_RUNS_PER_TEST
Default runs per scenario. -
LLM_ACCURACY_THRESHOLD
Suite-wide accuracy requirement. -
LLM_RAW_DUMP
never|fail|always. -
LLM_RAW_DUMP_DIR
Directory to save raw outputs.
🧪 Writing Tests
Scenarios go in scenarios/*.py. Each test chains prompts and expectations.
from speculate import Scenario
from speculate.providers import OllamaProvider
from models.greeting import GreetingResponse
provider = OllamaProvider(model="mistral")
# JSON schema validation
Scenario("test_json_greeting", provider)\
.set_system_prompt("Always respond in JSON.")\
.prompt("Return greeting='Hello' and name='Donavan'.")\
.expect_schema(GreetingResponse, greeting="Hello", name="Donavan")\
.runs(3)\
.require_accuracy(0.9)\
.dump_raw(mode="fail", to_dir="raw_outputs", file_format="json")\
.run()
# Contains expectation, random seed per run
Scenario("test_contains_name", provider)\
.randomize_seed(True)\
.prompt("Say hello to Donavan in one short sentence.")\
.expect_contains("Donavan")\
.runs(5)\
.run()
# Multi-step chained prompts
Scenario("test_multi_step", provider)\
.set_system_prompt("Answer tersely.")\
.prompt("Explain why the sky is blue.")\
.expect_contains("Rayleigh")\
.prompt("Is that wavelength-dependent?")\
.expect_contains("yes")\
.runs(2)\
.run()
# Single-shot: ignores second step
Scenario("test_single_shot", provider)\
.multi_shot(False)\
.prompt("THIS runs")\
.expect_contains("THIS")\
.prompt("THIS will be ignored")\
.expect_contains("ignored")\
.runs(3)\
.run()
Pydantic schema example:
# models/greeting.py
from pydantic import BaseModel
class GreetingResponse(BaseModel):
greeting: str
name: str
📊 Output Example
Each scenario prints:
- Header panel (scenario, mode, seed info, prompt preview).
- Per-run table (run #, PASS/FAIL badge, seed, details).
- Accuracy bar + threshold info.
- Suite summary at the bottom.
🛠 Extending
- Add other providers by implementing
generate(prompt, system_prompt, history, seed, **kwargs). - Export
RESULTSfromspeculate.coreas JSON or JUnit for CI pipelines. - Build fixtures for common prompts or expectations.
- Use
.expect_not_equal(...)and.expect_not_contains(...)for negative checks.
📦 Packaging
- Editable install for development:
pip install -e .
speculate scenarios/
📜 License
MIT License © 2025 Donavan White (digi-cazter)
Metadata
Release files for speculate-llm 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| speculate_llm-0.1.0.tar.gz | 135.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| speculate_llm-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 150.7 kB
Release files / speculate_llm-0.1.0.tar.gz
| Download URL | speculate_llm-0.1.0.tar.gz |
|---|---|
| Size | 135.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b45f95113355e8e4bcfd45a196f3c2a3c6b411e3d725079e7b3b3a782c4682da
|
|
BLAKE2b-256 checksum How to use checksums |
5fde845cf55af5e26bc77994037708ca34b7cb203aefcd9e88d6157c442c1069
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.13
|
Release files / speculate_llm-0.1.0-py3-none-any.whl
| Download URL | speculate_llm-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fa953529771735d4d3046daa94802d6c49eae1bc9ce4a32c9e649e5ce2fc8e9d
|
|
BLAKE2b-256 checksum How to use checksums |
ac34e313a0df03b50ef1aaae86353e69da76681df6f8598906e54d505421e968
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.11.13
|