toolcontract
Contract testing for LLM agent tool-calls.
When a provider ships a new model version, an agent's tool-calling behavior can silently change — invented or dropped arguments, a different tool chosen for the same input, values that no longer match a schema your downstream code depends on. Nothing in a typical CI pipeline catches this before it reaches production.
toolcontract is built around one trigger event: a model version changed
— did your agent's tool calls still do what you expect? Pin a golden set
of expected tool-call trajectories, run them against a live model, get a
pass/fail/inconclusive verdict and a diff. Think Pact for microservice
contracts, or Percy for visual regressions, but for tool calls.
Status
v0.1, in active development. Contract model, comparators/matching engine,
verification ledger, OpenAI/Anthropic/LiteLLM adapters, the CLI
(run/accept/check-version), a real pytest plugin, and the LLM-judge
semantic tier are all built and tested. Not yet published to PyPI.
Quick start
from toolcontract import ArgKind, ArgSpec, Contract, ExpectedCall
from toolcontract.adapters.base import ToolSchema
from toolcontract.adapters.openai_adapter import OpenAIAdapter
from toolcontract.runner.engine import run_contract
log_water = ToolSchema(
name="log_water",
description="Log that the user drank water",
parameters={
"type": "object",
"properties": {"amount_ml": {"type": "number"}},
"required": ["amount_ml"],
"additionalProperties": False,
},
strict=True,
)
contract = Contract(
id="log-water-basic",
description="User mentions drinking water, agent logs it",
input_messages=({"role": "user", "content": "I just drank a glass of water"},),
expected_calls=(
ExpectedCall(tool_name="log_water", args={"amount_ml": ArgSpec.exact(250, kind=ArgKind.NUMBER, tolerance=20)}),
),
)
result = run_contract(contract, OpenAIAdapter(), [log_water], "gpt-4o")
print(result.verdict) # Verdict.PASS / FAIL / INCONCLUSIVE
Or from the CLI, against a directory of compiled contracts:
toolcontract run contracts/ --provider openai --model gpt-4o --tools tools.json
toolcontract accept contracts/one.json --provider openai --model gpt-4o # promote a new baseline
toolcontract check-version contracts/ --provider openai --model gpt-4o-2027 --ledger .toolcontract_ledger.json
See examples/tap_health_agent_get_meal_suggestions.py for a real contract
authored against a production tool schema, including a custom comparator
and an optional argument.
Providers
- OpenAI, Anthropic — native adapters,
pip install "toolcontract[openai]"/[anthropic]. - Any OpenAI-compatible endpoint — a self-hosted LiteLLM proxy, vLLM,
Ollama, OpenRouter, Azure OpenAI — already works today with zero new code:
import openai from toolcontract.adapters.openai_adapter import OpenAIAdapter adapter = OpenAIAdapter(client=openai.OpenAI(base_url="http://localhost:4000", api_key="sk-..."))
- LiteLLM (direct SDK routing) —
pip install "toolcontract[litellm]", thenLiteLLMAdapter()with a provider-prefixed model string ("cerebras/zai-glm-4.7","gemini/gemini-2.0-flash", and 100+ others LiteLLM supports). Capability (whether the underlying provider actually honors strict/grammar-constrained tool calling) is queried per-model at call time, not assumed statically — seeadapters/litellm_adapter.py's module docstring for why, and for the honest caveat about how precise that signal currently is. - Missing your provider? See
CONTRIBUTING.md— adding a native adapter is the single most valuable contribution this project can take right now.
Non-goals
This is deliberately not a general agent evaluation framework. If a
request is really about one of the following, a different tool is the
right fit — agentevals, DeepEval, Ragas, PydanticAI Evals,
LangSmith, or Phoenix all already cover this territory well:
- RAG retrieval or answer-quality evaluation
- Hallucination detection or general output-quality scoring
- Multi-turn simulated-user agent evaluation
- General-purpose "score my agent" metrics unrelated to tool-call structure
toolcontract owns exactly one job: did this model version's tool-calling
behavior change against a pinned contract. Everything in scope should trace
back to that job — anything that doesn't belongs in a different package.
Design principles (from the architecture review)
- Core engine is framework-agnostic. The pytest plugin is a thin consumer of a plain Python/CLI-callable core — not the other way around.
- Three-valued verdicts.
PASS/FAIL/INCONCLUSIVE. An argument that can't be resolved structurally (e.g. free-form natural language) reportsINCONCLUSIVE, never a falseFAIL. - Type-aware comparison, not naive equality. Dates, numbers, and strings are normalized before comparison so formatting variance doesn't produce false regressions.
- Contracts compile to portable, versioned JSON. Python is the authoring ergonomics; the JSON file is the diffable artifact checked into a repo and hashed for the verification ledger.
- Custom comparators are registered, never dynamically imported.
A contract file's
comparator_refmay not share the trust level of the code running it — a PR from an external contributor, or a contract pulled from a shared source.@toolcontract.register_comparatorrequires the trusted test suite to explicitly opt a function in; anything else in a contract file fails closed instead of being imported and called. - Capability is per-model, not per-adapter. A single adapter instance
(especially a gateway-style one like
LiteLLMAdapter) can front providers with genuinely different tool-calling guarantees —capability_for(model)is queried at call time, never assumed as a static fact.
Contributing
See CONTRIBUTING.md — provider adapters especially welcome.
Development
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
Metadata
Release files for toolcontract 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 | Provenance | Actions |
|---|---|---|---|---|
| toolcontract-0.1.0.tar.gz | 41.8 kB | Without provenance | Details for toolcontract-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toolcontract-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for toolcontract-0.1.0-py3-none-any.whl |
Total release size: 78.9 kB
Release files / toolcontract-0.1.0.tar.gz
| Download URL | toolcontract-0.1.0.tar.gz |
|---|---|
| Size | 41.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
49d81b1c4fa2977964170fa559e3913153b6595895fa7d0261e6df9623db613d
|
|
BLAKE2b-256 checksum How to use checksums |
7726b201ac9cfb9ef7d152bd8d591086950248b617d17edf3f37e4f90a9b25c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / toolcontract-0.1.0-py3-none-any.whl
| Download URL | toolcontract-0.1.0-py3-none-any.whl |
|---|---|
| Size | 37.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df97e270009e9776d628182628a9a55019197009e6ed67321fb19c6b102e44df
|
|
BLAKE2b-256 checksum How to use checksums |
8e5f35818dc562c87d1fca446637f025ea7af8c827237af1ca9e439af8738153
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for toolcontract 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.