Configuration
One file,tacit_deid.yaml, that you own. load_config(path) returns an SdkConfig; the engine
reads engine_config() and the model client reads llm_config(). Credentials are never in the
file.
Fields
A file missing
model or allowed_endpoint_hosts, or with a non-list value for either, fails at
load, before the pipeline runs.
The endpoint allowlist
The large language model (LLM) seam is the one place the transcript is a model’s input, so where that call goes is bounded up front. When the client is constructed, before any Protected Health Information (PHI) is sent, the SDK resolves the host the configured model would call and checks it againstallowed_endpoint_hosts. A host passes when it equals an entry or is a subdomain of one,
with a spoof guard. Otherwise EndpointNotAllowedError is raised and no client exists. The
resolved host is recorded in the artifact’s audit log.
How the host is resolved: from base_url when set; for a bedrock/ model,
bedrock-runtime.<region>.amazonaws.com; for ollama, localhost; for openai,
api.openai.com; otherwise the provider tag itself, still checked against the list.
Detector stacks
detectors names the Stage-0 members. Their spans are unioned; on an overlap a structured entity
beats a generic one.
The default is
[presidio, gliner-pii-open]. In 294 synthetic health records, Presidio alone
found 77.4% of the planted identifiers and the default stack found 97.0%; in 20 synthetic doctor
and patient dialogs, 44.9% against 65.2% (Tacit’s detector study of 27 September 2026, one run
each). The model catches names, occupations, organisations, and record numbers Presidio does not
see. No GLiNER model is bundled: the default needs the gliner extra, and gliner-pii-open
downloads about 0.7 GB on first use, or loads from a local directory named by
TACIT_DEID_GLINER_PII_OPEN_MODEL. Without the extra, the first command stops and names the fix.
Every detector obeys the same guards. A clinician role (“the GP”, “a consultant”) and a generic
department or facility (“A&E”, “gastroenterology”) stay in the text, because they say who treated
the patient and where; a patient’s own occupation (“I’m a doctor”) and a named institution
(“Christchurch Hospital”) are replaced.
To run without a model detector, set detectors: [presidio]. A run or roundtrip without a
model member prints one warning to stderr that names the gap.
What each stack detects
What Stage 0 misses is the judge loop’s to catch, and a mid-sized judge does not always catch it.
If your transcripts carry occupations, record numbers, or organisations, stack
gliner-pii. Stack
it before you turn on send_tokenized_transcript, because then the cleaned transcript itself
crosses, not only the beats drawn from it.
Formats the shipped stack does not know, your record numbers and ward names, go in
custom_identifiers; see Your own identifier formats.
Every detector obeys the same three rules, whatever its model scores: a bare role word that
labels a turn (Doctor:, [patient]) is never an identifier, a generic work word (work,
job, site) is never an occupation, and a relative time phrase (last night, Monday,
six months ago) is never a date. A label that holds a name (Dr Salesa:) is still replaced.
Environment variables
run and roundtrip take --env-file PATH to load these from a KEY=VALUE file first; a
variable already set keeps its value.
Engine knobs, in one line each
boundary_k is written into the audit log and enforced by singling-out. prevalence_floor is the
clinical-rarity cutoff; it flags only a condition the tables know and rate below the floor
everywhere. Besides the national tables, it reads about 10,800 names and synonyms of disorders
Orphanet confirms are rare, each at the highest prevalence Orphanet records for it anywhere, so a
beat naming Marfan syndrome or Huntington’s disease is dropped. Orphanet records mostly European
figures: a condition common elsewhere, such as malaria, counts as rare in the four supported
countries. max_generalization_loops bounds the judge loop; exceeding it is a refusal, not a
pass. mattr_window shapes one distributional target. Lowering boundary_k or raising the loop
cap to make a refused record pass defeats the control that refused it.