Skip to main content

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]", then LiteLLMAdapter() 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 — see adapters/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) reports INCONCLUSIVE, never a false FAIL.
  • 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_ref may 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_comparator requires 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

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)

Source distribution for toolcontract 0.1.0
File Size Uploaded
toolcontract-0.1.0.tar.gz 41.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolcontract 0.1.0
File Interpreter ABI Platform
toolcontract-0.1.0-py3-none-any.whl Python 3 none any Details

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

Release history Release notifications | RSS feed

This release

0.1.0 This release

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