llmground
Your schema validated. The value was still made up.
Structured output libraries guarantee the shape of extracted data. None of them check whether the values are actually in the document. llmground runs after extraction and tells you which fields are real.
from llmground import verify
report = verify(extracted, source_document)
if report.has_hallucinations:
route_to_human_review(report.ungrounded)
No API calls. No model. No cost. Zero dependencies.
The bug this catches
extracted = {
"vendor": "ACME Manufacturing Ltd.",
"total": 2846.25, # the invoice says 2,486.25
}
Right types. Passes any JSON schema. Passes Pydantic. The number is a transposition the model invented, and it goes straight into your accounting system. Shape validation cannot see this, because there is nothing wrong with the shape.
GROUNDED vendor = 'ACME Manufacturing Ltd.'
found: ACME Manufacturing Ltd. 14 Sundar Industrial Estate...
UNGROUNDED total = 2846.25
nearest number in source is '$2,486.25'
6/8 fields grounded (75%), 0 weak, 2 ungrounded
Install
pip install llmground
It works with what you already use
llmground doesn't replace Instructor, Outlines, or PydanticAI. It runs after them. Pydantic models, dataclasses, and plain dicts all work, and Pydantic is optional rather than required.
import instructor
from llmground import verify
invoice = client.chat.completions.create(response_model=Invoice, ...)
report = verify(invoice, source_pdf_text)
if report.grounding_rate < 0.9:
escalate(report)
Why matching values is harder than it looks
The naive version of this check is str(value) in source, and it produces so
many false alarms that people switch it off within a day. Real documents don't
write values the way models return them.
| Model returns | Document says | Naive check | llmground |
|---|---|---|---|
4500.0 |
$4,500.00 |
miss | matched |
"2026-01-05" |
5 January 2026 |
miss | matched |
"O'Brien Ltd" |
O'Brien Ltd (smart quote) |
miss | matched |
1234.56 |
1.234,56 (European) |
miss | matched |
-1234.00 |
(1,234.00) (accounting) |
miss | matched |
"ACME Inc" |
ACME, Inc. |
miss | matched |
Every one of these is handled deterministically, with no model in the loop.
Verdicts
| Verdict | Meaning |
|---|---|
grounded |
Found in the source |
weak |
Found only by fuzzy match. A human should look. |
ungrounded |
Not in the source. Likely fabricated. |
skipped |
Not verifiable: booleans, nulls, values like "1" or "yes" |
Fuzzy matches are weak, not grounded, by default. A 90%-similar string
is a different string, and on a financial or medical field that difference is
the entire problem. Opt in with accept_fuzzy=True when you're handling OCR
noise and know what you're trading away.
Skipped fields are excluded from the grounding rate. Counting booleans as grounded would inflate the score and hide real problems.
Spans, for human review
Every match reports character offsets into the source, so you can highlight the evidence rather than just flagging a field.
for r in report.grounded:
span = r.match.spans[0]
highlight(source, span.start, span.end)
That's the difference between telling a reviewer "this field is suspicious" and showing them why.
CLI
llmground extracted.json --source invoice.txt --fail-under 0.9
Exits non-zero below the threshold, so it drops into CI as a quality gate.
Options
verify(
extracted,
source,
accept_fuzzy=False, # count fuzzy matches as grounded
fuzzy_threshold=0.85, # similarity floor for reporting a fuzzy match
number_tolerance=0.01, # allowed numeric difference
day_first=True, # how to read 03/04/2026
ignore=["metadata.run_id"], # skip computed fields
)
Limitations
- Extractive only. It verifies values that should appear in the source. A
field that's legitimately inferred rather than copied (a category label, a
computed total) will read as ungrounded. Use
ignorefor those. - Ambiguous dates are not guessed.
03/04/2026returns no match unless you passday_first, because a silently wrong date is worse than an unverified one. - Grounded is not correct. It confirms a value appears in the document, not that the model pulled it from the right place. A total that matches a different line item's amount will pass.
- Text sources only. PDFs and images need to be through OCR first.
Development
pip install -e ".[dev]"
pytest
MIT.
Metadata
Release files for llmground 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 |
|---|---|---|---|---|
| llmground-0.1.0.tar.gz | 21.2 kB | Without provenance | Details for llmground-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| llmground-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for llmground-0.1.0-py3-none-any.whl |
Total release size: 39.2 kB
Release files / llmground-0.1.0.tar.gz
| Download URL | llmground-0.1.0.tar.gz |
|---|---|
| Size | 21.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
17149cae253ede3f0c3dbc8e09532beeea7bab804ef999202072a250f971d8e8
|
|
BLAKE2b-256 checksum How to use checksums |
5a089609cf24ea02d8e5fc51c6e357fc011520d471df305a39b929fcc27147f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / llmground-0.1.0-py3-none-any.whl
| Download URL | llmground-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd1f335a00e8a3de5eee6e1452cac54660cbc2792f98a7d94337e594050f1646
|
|
BLAKE2b-256 checksum How to use checksums |
8313a2d3a96eb5a1f2df671ff1aad3f53432fd5dd756869573a00ff2165ffecb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Security for llmground 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.