Natural Language Inference (NLI)¶
Verified against v2.1.2-ce. Every request and response on this page was executed against a running v2.1.2-ce gateway; outputs are real, truncated only where marked.
SynapCores ships an in-process NLI cross-encoder for claim verification:
given a statement and one or more pieces of trusted evidence (truth
anchors), it answers TRUTH, LIE, or UNVERIFIED_PLAUSIBLE. The flagship
use is hallucination-gating: verify an LLM or agent answer against the
retrieval set it was built from, before you store or display it.
The same pinned model serves three surfaces — REST endpoints, SQL functions (v2.1.2+), and the SQL result feeds your application — with no sidecar, no network hop, and no external API.
- Model:
cross-encoder/nli-deberta-v3-small(DeBERTa-v3), FP32 ONNX, pinned by SHA-256 at install and verified at load. Labels:contradiction/entailment/neutral; 512-token window. - Performance (v2.1.2+, measured on a 6-core i5-10400F, CPU): ~15 ms per warm single-anchor verify; 16 anchors ≈ 192 ms; 32 anchors ≈ 335 ms. Engines before v2.1.2 cost ~0.37 s per pair — same scores, 26–33× slower.
Setup¶
NLI is off until a model bundle is installed and the gateway restarts with it present. The runtime loads models from a process-start snapshot: pulling a bundle while the gateway runs does not make it servable.
Docker¶
docker run -d -p 8080:8080 \
-v synapcores-data:/var/lib/synapcores \
-e AIDB_ACCEPT_LICENSE=1 \
-e AIDB_NLI_ENABLED=1 \
synapcores/community:latest
The entrypoint pulls nli-default (~576 MB, one time, SHA-pinned) before
starting the server when AIDB_NLI_ENABLED=1. Nothing else to do.
Bare metal (tarball / installer)¶
Pull the bundle before the gateway starts, using the same --config the
service uses, then start (or restart) the gateway:
synapcores --config /etc/synapcores/gateway.toml pull nli-default
# installed NLI bundle: {"alias":"nli-default","id":"nli-deberta-v3-small-fp32", ...}
systemctl restart synapcores
Enable via environment (AIDB_NLI_ENABLED=1, optional
AIDB_NLI_MODEL=nli-default) or the [nli] config section. Check disk-side
state any time with synapcores --config <cfg> models status nli-default;
runtime readiness is only ever reported by GET /v1/ai/nli/models.
Upgrade the whole folder
The engine loads ONNX Runtime from lib/ beside the executable.
Copying only the binary during an upgrade breaks NLI model loads; since
v2.1.2 the error names the three searched paths. Upgrade the whole
extracted folder, or set ORT_DYLIB_PATH.
Model catalog¶
| Alias | Upstream (pinned revision) | Download | Notes |
|---|---|---|---|
nli-default, nli-small |
cross-encoder/nli-deberta-v3-small @ fa28048 |
~576 MB | the two aliases share one artifact |
nli-xsmall |
cross-encoder/nli-deberta-v3-xsmall @ a150876 |
~284 MB | smaller footprint for tight memory |
All bundles are Apache-2.0, immutable, and content-verified: the installer checks SHA-256 on download, and the loader re-verifies before serving. A new alias becomes servable only after a gateway restart.
REST API¶
Three endpoints, all under the authenticated AI router (JWT or
aidb_… API key as Authorization: Bearer …; standard body-size and rate
limits apply). All success responses use the gateway envelope
{"data": …, "meta": …}; errors use {"error": {"code", "message"}, "meta"}.
| Purpose | Endpoint | Method |
|---|---|---|
| Runtime model state | /v1/ai/nli/models |
GET |
| Verify a statement against anchors | /v1/ai/nli/verify |
POST |
| Score premise/hypothesis pairs | /v1/ai/nli/pairs |
POST |
Both POST bodies reject unknown fields (deny_unknown_fields) — a misspelled
key is a 400, never silently ignored.
GET /v1/ai/nli/models — readiness and identity¶
{
"data": {
"enabled": true,
"default_model": "nli-default",
"restart_required_for_new_installs": true,
"models": [
{
"model": "nli-default",
"state": "ready",
"identity": {
"alias": "nli-default",
"id": "nli-deberta-v3-small-fp32",
"repository": "cross-encoder/nli-deberta-v3-small",
"revision": "fa2804872c3b4bd748f38c0185cc85775361e735",
"license": "Apache-2.0",
"bundle_sha256": "3d809ec29d1a31af5db11075598a7c89ef4ba42fd29daad6ca80d25d7373d768",
"labels": ["contradiction", "entailment", "neutral"],
"max_length": 512
},
"loaded_bundle_sha256": "3d809ec2…"
}
]
}
}
identity also reports exact byte counts and free-space guidance;
a ready model adds an execution capability profile.)
state is one of ready, missing, failed, or an intermediate load
state. Poll this after install/restart; serve traffic only on ready.
POST /v1/ai/nli/verify — the verification verdict¶
Request fields:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
statement |
string | yes | — | the claim to check, ≤ 16,384 UTF-8 bytes |
truth_anchors |
string[] | yes | — | 1–32 trusted evidence texts, each ≤ 16,384 bytes. The field is truth_anchors — anchors is rejected. |
model |
string | no | nli-default |
an installed alias |
closed_world |
bool | no | false |
true: anything the anchors don't entail is a LIE |
contradiction_threshold |
number | no | 0.6 |
score needed to call a contradiction |
entailment_threshold |
number | no | 0.6 |
score needed to call an entailment |
curl -s http://localhost:8080/v1/ai/nli/verify \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{
"statement": "The rollout completed without failures.",
"truth_anchors": [
"The deployment finished at 14:02 with zero failed checks.",
"All 46 post-deploy probes passed."
]
}'
{
"data": {
"model": "nli-default",
"bundle_sha256": "3d809ec2…",
"verdict": "TRUTH",
"reason": "An anchor reaches the entailment threshold and no anchor reaches the contradiction threshold.",
"confidence": 0.9977391796557792,
"violated_anchor": null,
"matched_anchor": "The deployment finished at 14:02 with zero failed checks.",
"all_scores": [
[0.0001135, 0.9977392, 0.0021473],
[0.0023965, 0.9266143, 0.0709892]
]
}
}
all_scores is one [contradiction, entailment, neutral] triple per anchor,
in request order. The statement is checked against each anchor
independently — two anchors are never chained into a multi-step proof.
The three verdicts, observed:
| Verdict | When | Observed reason |
|---|---|---|
TRUTH |
some anchor entails the statement, none contradicts it | "An anchor reaches the entailment threshold and no anchor reaches the contradiction threshold." |
LIE |
some anchor contradicts it — contradiction always wins over entailment | "An anchor reaches the contradiction threshold; contradiction takes precedence over entailment." |
UNVERIFIED_PLAUSIBLE |
no anchor reaches either threshold (open world) | "No anchor reaches either threshold; open-world policy leaves the statement unverified." |
With "closed_world": true the third case becomes a LIE:
{
"verdict": "LIE",
"reason": "Closed-world policy rejects a statement without qualifying entailment; no anchor reaches the contradiction threshold.",
"confidence": 0.9994635870868093
}
Use closed-world when the anchors are a complete allow-list (a policy, a spec); use the open-world default when they are merely what you happened to retrieve.
POST /v1/ai/nli/pairs — raw classification scores¶
For when you want probabilities, not a verdict — ranking, custom thresholds, batch screening. 1–32 pairs per call.
curl -s http://localhost:8080/v1/ai/nli/pairs \
-H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{
"pairs": [
{"premise": "The invoice was paid on March 3.", "hypothesis": "The invoice is unpaid."},
{"premise": "The invoice was paid on March 3.", "hypothesis": "Payment was received in March."}
]
}'
{
"data": {
"model": "nli-default",
"bundle_sha256": "3d809ec2…",
"scores": [
{"contradiction": 0.9994857, "entailment": 0.0003246, "neutral": 0.0001897},
{"contradiction": 0.0001434, "entailment": 0.9962150, "neutral": 0.0036416}
]
}
}
The premise is the evidence, the hypothesis is the claim — direction
matters to an NLI model.
Error reference (all observed)¶
| HTTP | code |
Trigger | Observed message |
|---|---|---|---|
| 400 | invalid_nli_request |
unknown/misspelled field (e.g. anchors instead of truth_anchors) |
"Invalid NLI verification JSON request" |
| 400 | invalid_nli_request |
0 or > 32 anchors | "Invalid argument: NLI verification requires between 1 and 32 anchors" |
| 400 | invalid_nli_request |
a text over 16,384 UTF-8 bytes | names the offending statement/anchor |
| 404 | nli_model_not_found |
unknown alias, or pulled after start | "The NLI model alias is unknown; newly installed aliases require a gateway restart" |
| 503 | nli_disabled |
NLI not enabled | "NLI is disabled; install a model and enable [nli] or AIDB_NLI_ENABLED" |
| 503 | nli_model_not_installed |
enabled but bundle absent at process start | "…pull or import it and restart the gateway" |
| 503 | nli_model_unavailable |
bundle failed integrity/load | "…reinstall and restart the gateway" |
| 401 | missing_claims |
no/invalid auth | "Authentication required" |
Model aliases are 1–128 ASCII letters, digits, _ or -, starting with a
letter or digit.
SQL functions (v2.1.2-ce+)¶
The same model, callable inside queries — see the SQL reference for full semantics. NLI results are never served from the SQL result cache.
NLI_CLASSIFY(model, premise, hypothesis) → JSON — one pair's scores:
SELECT NLI_CLASSIFY('nli-default',
'The invoice was paid on March 3.',
'The invoice is unpaid.') AS scores;
-- {"model":"nli-default","bundle_sha256":"3d809ec2…",
-- "scores":{"contradiction":0.99949,"entailment":0.00032,"neutral":0.00019}}
NLI_VERIFY(model, statement, anchors_json [, options_json]) → JSON —
the same verdict object as the REST endpoint. Options keys: closed_world,
contradiction_threshold, entailment_threshold.
SELECT NLI_VERIFY('nli-default',
'The rollout completed without failures.',
JSON_ARRAY('The deployment finished at 14:02 with zero failed checks.',
'All 46 post-deploy probes passed.'),
JSON_OBJECT('closed_world', true)) AS v;
-- {"verdict":"TRUTH","confidence":0.99774,
-- "matched_anchor":"The deployment finished at 14:02 with zero failed checks.", …}
NLI_ANCHORS(text) — an aggregate that collects up to 32 rows into an
anchors array, built for feeding NLI_VERIFY from evidence rows:
CREATE TABLE IF NOT EXISTS run_evidence (id INT PRIMARY KEY, run_id INT, fact TEXT);
INSERT INTO run_evidence VALUES
(1, 7, 'The deployment finished at 14:02 with zero failed checks.'),
(2, 7, 'All 46 post-deploy probes passed.'),
(3, 7, 'Rollback was not required.');
SELECT NLI_VERIFY('nli-default',
'The rollout completed without failures.',
NLI_ANCHORS(fact)) AS verdict
FROM run_evidence WHERE run_id = 7;
-- {"verdict":"TRUTH","confidence":0.99774, …}
Like the other AI functions, call it through an alias rather than directly in
ORDER BY (sort by the alias).
The guardrail pattern¶
Gate what an agent stores or shows: verify the answer against the retrieval set it was built from, in one statement.
-- store the answer only if the evidence entails it
INSERT INTO approved_answers (run_id, answer, verdict)
SELECT 7, $answer,
NLI_VERIFY('nli-default', $answer, NLI_ANCHORS(fact))
FROM run_evidence
WHERE run_id = 7
AND JSON_EXTRACT(NLI_VERIFY('nli-default', $answer, NLI_ANCHORS(fact)),
'$.verdict') = 'TRUTH';
Over REST the same shape is: retrieve evidence → POST /v1/ai/nli/verify
with the evidence as truth_anchors → act on verdict.
Honest boundaries¶
- The model evaluates the text you supply. It does not retrieve facts, resolve pronouns across anchors, or compare numbers and dates with typed precision — use exact SQL comparisons for those.
- Anchors are checked independently; there is no multi-anchor chaining.
- Contradiction precedence means one contradicting anchor outvotes any number of entailing ones — curate anchor sets, don't dump whole documents.
- Scores describe model behavior over your text and thresholds. Calibrate the thresholds on your own data before gating anything irreversible.
- Limits: 32 anchors / 32 pairs per call, 16,384 UTF-8 bytes per text, 512-token model window (longer inputs are truncated by the tokenizer).
See also¶
- SQL reference — NLI functions · Release notes v2.1.2-ce
- Worked recipes: Semantic claim verification and the eight companion NLI recipes on the recipe catalog