Cross-provider schema contract testing for LLMs. Define once, validate everywhere — OpenAI, Anthropic, Mistral, LiteLLM and any JSON-returning model.
Project description
llmcontract
Cross-provider schema contract testing for LLMs.
Define the expected shape of an LLM response once. Validate it against OpenAI, Anthropic, Mistral, LiteLLM, or any model that returns JSON — with a single, consistent API.
from llmcontract import Contract, field, required_fields
contract = Contract(
required_fields("action", "confidence", "reasoning"),
field("action", type=str, values=["buy", "sell", "hold"]),
field("confidence", type=float),
field("reasoning", type=str, min_len=20),
)
# Works with any provider
from llmcontract import openai_response
data = openai_response(client.chat.completions.create(...))
contract.validate(data) # raises ContractViolation on mismatch
Why llmcontract?
| Pain | llmcontract fix |
|---|---|
Provider A returns {"action": "BUY"}, provider B returns {"signal": "buy"} |
One contract definition catches both regressions |
| LLM adds unexpected keys that break downstream code | no_extra_fields() validator |
| Confidence field is sometimes a string, sometimes a float | field_type() catches it every time |
| Nested tool-call responses are hard to validate | nested() validator recurses cleanly |
| You switch providers and don't know what broke | Run the same contract against the new provider |
Install
pip install llmcontract
No hard dependencies. Provider SDKs are optional:
pip install "llmcontract[openai]"
pip install "llmcontract[anthropic]"
pip install "llmcontract[all]"
Quickstart
1. Define a contract
from llmcontract import Contract, field, required_fields, no_extra_fields
trade_contract = Contract(
required_fields("action", "confidence"),
field("action", type=str, values=["buy", "sell", "hold"]),
field("confidence", type=float),
field("reasoning", type=str, min_len=10, required=False),
no_extra_fields("action", "confidence", "reasoning"),
name="TradeSignal", # optional, shown in error messages
)
2. Validate an OpenAI response
import openai
from llmcontract import openai_response
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Give me a trade signal as JSON."}],
)
data = openai_response(response)
trade_contract.validate(data)
3. Validate an Anthropic response
import anthropic
from llmcontract import anthropic_response
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=256,
messages=[{"role": "user", "content": "Give me a trade signal as JSON."}],
)
data = anthropic_response(response)
trade_contract.validate(data)
4. Validate a raw JSON string
from llmcontract import raw_json
text = '```json\n{"action": "buy", "confidence": 0.87}\n```'
data = raw_json(text) # strips fences, parses JSON
trade_contract.validate(data)
Validators
| Validator | What it checks |
|---|---|
required_fields(*names) |
All named fields exist |
field_type(name, type) |
Field is a specific Python type |
enum_values(name, allowed) |
Field value is in a list of allowed values |
min_length(name, n) |
String/list has at least n chars/items |
max_length(name, n) |
String/list has at most n chars/items |
regex_match(name, pattern) |
String field matches a regex |
no_extra_fields(*allowed) |
Response has no unexpected keys |
nested(name, contract) |
Sub-dict satisfies a child contract |
field(name, ...) |
All of the above in one call |
field() — the Swiss Army knife
field(
"email",
required=True, # default True
type=str,
pattern=r"^[^@]+@[^@]+\.[^@]+$",
min_len=5,
max_len=254,
)
Strict mode — collect all violations
By default, validate() raises on the first failure. In strict mode it
collects every failure and raises a single combined error:
contract = Contract(
required_fields("a", "b", "c"),
strict=True,
)
try:
contract.validate({})
except ContractViolation as exc:
print(exc)
# Contract [MyContract] violated with 3 error(s):
# • Required field missing: 'a'
# • Required field missing: 'b'
# • Required field missing: 'c'
Extend a contract
base = Contract(required_fields("id", "name"))
strict_version = base.extend(
field("id", type=int),
field("name", type=str, min_len=1),
no_extra_fields("id", "name"),
)
Nested responses
from llmcontract import Contract, nested, required_fields, field
address_contract = Contract(
required_fields("street", "city", "country"),
field("country", type=str, min_len=2, max_len=2), # ISO 3166-1 alpha-2
)
person_contract = Contract(
required_fields("name", "address"),
field("name", type=str),
nested("address", address_contract),
)
person_contract.validate({
"name": "Alice",
"address": {"street": "42 Main St", "city": "Springfield", "country": "US"},
})
Use with pytest
# tests/test_my_llm.py
import pytest
from llmcontract import Contract, required_fields, field
@pytest.fixture
def contract():
return Contract(
required_fields("answer", "confidence"),
field("confidence", type=float),
)
def test_response_shape(contract, mock_llm_response):
data = raw_json(mock_llm_response)
contract.validate(data) # pytest shows ContractViolation as assertion error
Exceptions
All exceptions inherit from ContractViolation(AssertionError):
FieldMissingError— required field absentFieldTypeError— wrong Python typeEnumViolationError— value not in allowed listLengthViolationError— string/list too short or too longPatternViolationError— regex did not matchExtraFieldError— unexpected keys present
Provider adapters
| Adapter | Usage |
|---|---|
openai_response(resp) |
openai>=1.0 ChatCompletion objects |
anthropic_response(resp) |
anthropic>=0.20 Message objects |
litellm_response(resp) |
LiteLLM ModelResponse (OpenAI-compatible) |
raw_json(text) |
Raw JSON string, strips code fences |
raw_dict(data) |
Pass-through for already-parsed dicts |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file providercontract-0.1.0.tar.gz.
File metadata
- Download URL: providercontract-0.1.0.tar.gz
- Upload date:
- Size: 12.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd2beabf261060f7ba546b12e69e6e0d3303e007d27f2f00f0dae3274e2d1fe3
|
|
| MD5 |
20138e18ec1ed49873f89e4dec216d29
|
|
| BLAKE2b-256 |
15fdb434bedf2618ef0a6ace169dbfe3e63811ab07e58ed2869108069972c046
|
File details
Details for the file providercontract-0.1.0-py3-none-any.whl.
File metadata
- Download URL: providercontract-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df009a6ed4a0b26eef3c51ea9796aa5ade2f4a36ff20cbbd23ebd9be8e71de1a
|
|
| MD5 |
8db0666594785bc4ea683055d2d03982
|
|
| BLAKE2b-256 |
e4c8168933311c39a2d6a2b61ea2457c33074807c4e2fd8ab9b2aae8415d0a45
|