Detector Runtime Contract
Detection modes, guarantees, fallback behavior, quality expectations, and how to verify the runtime configuration
Detection Modes
OGuardAI operates in one of three detection modes, configured in oguardai.yaml:
Mode: builtin (Deterministic)
detector:
mode: builtinGuarantees:
- Detection stays in process: pure Rust regex and string work, no network hop and no model inference
- Builtin entity types: the named formats plus the always-on builtin_custom detectors, detected via the regex registry
- Zero external dependencies
- Deterministic: same input always produces same output
- Horizontally scalable, no shared state
Named builtin entity types: email, phone, ssn, iban, vat_id, credit_card, ip, url, order, customer_id, passport, health_id, date_of_birth, address
Always-on builtin_custom detectors:
money (ISO 4217 currency amounts), de_tax_id (German tax ID), de_sozialversicherung (German social security), quote_id (quote/offer IDs), case_ref (case references). These emit typed custom:<name> entities and run in every mode, including builtin. Source: crates/core/src/types.rs BUILTIN_CUSTOM_NAMES, registered in detector-builtins.
NOT available (requires NER): person, company, location
Mode: both (Full Coverage)
detector:
mode: both
advanced_url: http://localhost:9090Guarantees:
- The builtin entity types (named + builtin_custom) plus the NER-only types person, company, and location
- Person names, company names, locations detected via GLiNER/spaCy
- Graceful fallback: if NER sidecar is unreachable,
bothmode falls back to builtin-only - Fallback timeout: configurable (
detector.timeout_secs)
Additional entity types: person, company, location
Cost shape: the builtin path stays in process. Adding NER adds a synchronous round trip to the Python sidecar and a model forward pass, which dominates the request. No latency numbers are published here, because none have been produced by a reproducible, named run; see Benchmarks for the harness that measures your own hardware.
Fallback behavior when NER is unavailable:
- Server logs:
ner_service_unavailable_falling_back_to_builtin - Detection continues with builtin regex only
- person/company/location will NOT be detected
- Health endpoint shows:
builtin_and_ner (full entity detection)but actual capability degrades - No crash, no hang: bounded by the configured NER timeout (
detector.timeout_secs)
Mode: advanced (NER Only)
detector:
mode: advanced
advanced_url: http://localhost:9090NER is mandatory in this mode. Builtin regex still contributes structured detection, but NER cannot be bypassed: requesting builtin_regex alone is rejected, and if the Python sidecar is unavailable the request FAILS CLOSED with GUARDAI_DETECTION_UNAVAILABLE (503, retriable) rather than downgrading to builtin-only. Graceful fallback to builtin is a both-mode behavior only.
The same fail-closed rule applies whenever a loaded policy marks an entity type required_for (via detection.ner_labels, or the opt-in healthcare-hipaa template): the runtime gates on whether NER actually ran, not on the configured mode, so a required_for type that NER did not produce rejects the request instead of silently passing the input through.
Detection Mode Decision Tree
How to Verify Runtime Mode
# Check current mode (requires admin scope)
curl -H "X-API-Key: $GUARDAI_API_KEY" http://localhost:3000/v1/diagnostics | jq .detector_mode
# Check available entity types
curl -H "X-API-Key: $GUARDAI_API_KEY" http://localhost:3000/v1/capabilities | jq '.entity_types[].name'
# Check per-request mode (in transform response)
curl -X POST http://localhost:3000/v1/transform \
-H "X-API-Key: $GUARDAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"test","input_type":"text"}' | jq .detector_modeWhat Each Mode Covers
No precision or recall percentages are published. None have been produced by a reproducible, named run against a dataset this project can point to, and a rate without a run behind it is a guess. What the modes differ in is which types are reachable at all, and on what basis each is matched.
| Entity Type | Builtin | + GLiNER | Basis |
|---|---|---|---|
| Detected | Same | Regex, Unicode-aware | |
| Phone | Detected | Same | Regex, international plus per-locale formats |
| SSN | Detected | Same | Strict format match |
| IBAN | Detected | Same | Country code plus structure |
| Credit Card | Detected | Same | Regex plus Luhn check |
| Person Name | Not detected | Detected | Model inference, language-dependent |
| Company Name | Not detected | Detected | Model inference, context-dependent |
| Location | Not detected | Detected | Model inference, may overlap with Address |
The regex rows are deterministic: the same input always yields the same spans. The NER rows are model
output, so their quality varies with the loaded model, its revision, and the language.
GET /v1/capabilities reports a per-language detection_support level, and it withholds one entirely
unless the sidecar's facts earn it.
Overlap Precedence
When a regex span and an NER span cover the same text, the merge applies a deterministic precedence so the entity type is stable regardless of NER over-tagging:
- Structured regex types always win over NER. email, phone, ssn, iban, ip, url, order, passport, health_id, customer_id, and critically
credit_cardanddate_of_birthare precise, deterministic, high-value PII. If NER tags an overlapping span (for example a date read as an event or a card number read as an ID), the structured regex type is kept, not downgraded to the NER label. - NER wins for name-shaped types (person, company, address) over non-structured regex overlaps.
- Otherwise the longer span wins, then the higher-confidence span, with ties resolved deterministically.
NER Precision Floor (False-Positive Suppression)
Zero-shot NER (GLiNER) over-tags common function and greeting words as person/company/location entities. OGuardAI applies a built-in precision floor after NER, additive over the raw model output:
- A universal acronym stoplist (~50 entries: PII, API, CEO, IBAN, SWIFT, HR, SSN, ...) drops NER spans that are just a known acronym.
- A per-language noise floor, shipped as data (
crates/detector-builtins/data/ner_noise_stoplist.json, covering German, English, and Arabic today), suppresses NER spans whose tokens are all noise words, stopwords, or proclitics, including glued phrases the model tags as one entity. - A minimum-confidence floor for NER-only types (built-in 0.65) drops low-confidence names.
This floor is a precision floor, not a ceiling: a deployment may add acronyms, un-suppress a specific built-in acronym so an acronym-shaped real organization name is detected, or raise the confidence floor (stricter only). It can never lower the floor or weaken detection below the built-in baseline, and invalid overrides are rejected at startup.
Sidecar Wire
This describes the internal contract on the runtime-to-sidecar hop (the Python NER service at
advanced_url), not the public /v1/detect or /v1/health API.
- The sidecar
/detectendpoint rejects any undeclared request field with HTTP 422 during validation. The rejection happens before any inference runs and before the inference permit is touched, so an unexpected payload never reaches the model or consumes a concurrency slot. - A 422 rejection never echoes a submitted value back, so a refused request cannot leak the input it carried.
- The sidecar
/healthendpoint reports awire_contractversion, so an operator can read it and diagnose a wire mismatch between the runtime and the sidecar. The runtime does not consume it.
Recommendation
- For maximum speed + determinism: Use
builtinmode - For maximum coverage: Use
bothmode with healthy NER sidecar - For production safety: Configure health monitoring for NER sidecar availability
Token Protocol
The semantic token format, deterministic assignment, the per-token capability cap, and the three-stage repair that make restoration safe against a mutating LLM
Detector Capabilities
Entity detection capabilities by runtime mode, language support matrix, performance characteristics, and deployment modes