Versions and upgrades
piighost follows semantic versioning. A patch release changes a behaviour, a minor release adds components or options, and a major release changes the public API. A name marked deprecated keeps working, and keeps resolving to the current implementation, until the next major release.
Check the installed release, then upgrade:
python -c "import piighost; print(piighost.__version__)"
pip install -U piighost
# or, with uv
uv lock --upgrade-package piighostEvery release has an entry in CHANGELOG.md, with its features, its fixes and its breaking changes.
API stability
Not every public name carries the same guarantee. A stable name changes only on a major release. An experimental one can change on a minor release, and the CHANGELOG entry says so when it does.
Stable
| Surface | What it covers |
|---|---|
| Package root | Every name in piighost.__all__, that is AnonymizationPipeline, ThreadAnonymizationPipeline, Anonymizer, RegexDetector, ExactMatchDetector, CompositeDetector, ChunkedDetector, LabelCounterPlaceholderFactory, LabelHashPlaceholderFactory, Detection, Entity, Span, PIIGhostError |
| Ports and templates | Every Any* protocol and its Base* template, one pair per pipeline stage |
| Data models | Detection, Entity and Span, frozen dataclasses with a stable field set |
| Configuration | PipelineConfig, load_config, load_pipeline, load_thread_pipeline, and the keys listed in the configuration reference |
| Command line | piighost validate, piighost schema, piighost anonymize |
| LangChain integration | PIIAnonymizationMiddleware, ToolCallStrategy, InventedPlaceholderStrategy, EntityCreateByAssistantStrategy |
| Model detectors | Gliner2Detector, SpacyDetector, TransformersDetector, all three on the shared BaseNERDetector pass |
| Guard rails | DetectorGuardRail, which re-runs any detector on the output |
A minor release adds components, options and placeholder factories on top of these without changing them. An option added on a minor release always carries the previous behaviour as its default.
Experimental
| Surface | Why it can still move |
|---|---|
piighost.integrations.claude_code | A spike. No Claude Code hook can rewrite the assistant's displayed reply, and that gap is unresolved. De-identification also runs one call per text leaf rather than batched. |
piighost.integrations.llama_index | Recent, two components, and the shape of the query-engine wrapper has not yet been settled against real corpora. |
LLMDetector, LLMGuardRail | The prompt and the structured-output schema can be reshaped when a provider changes, because they depend on what that provider accepts. |
ModerationGuardRail | Bound to a third-party Mistral moderation API whose categories and thresholds are outside this project. |
BridgeDetector, AnySpanRunner | Recent, and the span payload it accepts from a runner has not yet been exercised against enough runners to be frozen. |
Gliner2PiiDetector | A preset. Its default labels follow one PII checkpoint, and move when that checkpoint does. |
PresidioDetector | An adapter over Presidio's analyzer. The recognizers and entity names belong to the Presidio project. |
Gliner2GuardRail | Recent, and its default task and labels follow one guardrail checkpoint. |
Pin an exact version if you build on an experimental surface, and read the CHANGELOG before a minor upgrade.
Deprecated
No name is deprecated in 2.0. The hub names of 1.x stay as plain aliases of the catalog names, see the hub section below. The other names 1.x kept for back-compatibility are removed, and listed below.
Security fixes land on the latest minor only, on the stable and the experimental surface alike. An older minor receives no fix. Staying current is therefore part of the contract.
Upgrading to 2.0
The hub becomes the catalog
The companion site that publishes reviewed pattern groups and whole pipeline configurations was called the hub. In 2.0 it is the piighost catalog, a name that matches the catalogs key of a regex detector config. The names in the library follow.
| 1.x | 2.0 |
|---|---|
hub:piighost/generic | catalog:piighost/generic |
PIIGHOST_HUB_URL | PIIGHOST_CATALOG_URL |
https://hub.piighost.dev | https://catalog.piighost.dev |
piighost.hub, pull(ref, hub=...) | piighost.catalog, pull(ref, catalog=...) |
RegexDetector.from_hub(ref, hub=...) | RegexDetector.from_catalog(ref, catalog=...) |
HubError and its subclasses | CatalogError, CatalogRefError, CatalogUrlError, CatalogUnreachableError, CatalogPayloadError |
~/.cache/piighost/hub | ~/.cache/piighost/catalog |
Code and configs written for 1.8 and later keep running, with no warning. A hub: reference reads as catalog: wherever a reference is accepted, in catalogs, in load_config, load_pipeline and load_thread_pipeline, and on the command line. PIIGHOST_HUB_URL is read when PIIGHOST_CATALOG_URL is unset. piighost.hub re-exports piighost.catalog under its 1.x names, so HubError is CatalogError, and RegexDetector.from_hub calls from_catalog.
The disk cache moves with the name. A pinned reference cached by 1.x under ~/.cache/piighost/hub is fetched once more, then kept under ~/.cache/piighost/catalog. The old folder can be deleted.
The regex pattern sets move to the catalog
piighost ships no pattern of its own. The piighost.components.detector.patterns module is gone, with its four catalogs, and the same sets are groups of the piighost catalog.
| 1.x | 2.0 |
|---|---|
GENERIC_PATTERNS, catalogs = ["generic"] | catalog:piighost/generic |
US_PATTERNS, catalogs = ["us"] | catalog:piighost/us |
EU_PATTERNS, catalogs = ["eu"] | catalog:piighost/eu |
FR_PATTERNS, catalogs = ["fr"] | catalog:piighost/fr |
# 1.x
from piighost.components.detector.patterns import FR_PATTERNS, GENERIC_PATTERNS
detector = RegexDetector({**GENERIC_PATTERNS, **FR_PATTERNS})
# 2.0
from piighost.catalog import pull
detector = RegexDetector.from_catalog("catalog:piighost/generic")
detector = RegexDetector(
{**pull("catalog:piighost/generic"), **pull("catalog:piighost/fr")}
)A config still naming generic, us, eu or fr is refused at load time with the reference that replaces it. A reference pinned to a commit is fetched on the first build and read from the disk cache afterwards. A pipeline therefore reaches the network once. The catalog groups have moved on since the 1.x sets were copied. The group us carries US_ITIN, fr carries FR_SIREN, and the email pattern of generic takes Latin letters only. piighost anonymize with no config runs catalog:piighost/generic.
The 1.x aliases are removed
| Removed | Use instead |
|---|---|
piighost.integrations.middleware | piighost.integrations.langchain |
AssistantEntityStrategy | EntityCreateByAssistantStrategy |
the piighost[middleware] extra | piighost[langchain] |
# 1.x
from piighost.integrations.middleware import AssistantEntityStrategy, PIIAnonymizationMiddleware
# 2.0
from piighost.integrations.langchain import (
PIIAnonymizationMiddleware,
)BridgeDetector takes an offset unit
offset_unit is a required keyword, OffsetUnit.CODE_POINT for a runner written in Python, OffsetUnit.UTF16 for one written in JavaScript. A JavaScript runner counts an emoji as two units. Its offsets therefore used to land one character late after each emoji. An offset that is not an integer, 8.0 included, now raises BridgePayloadError instead of being truncated. A span scored below threshold is dropped, even when the runner ignored that threshold. See the detectors reference.
A thread is always named
require_thread_id is removed from the LangChain middleware. A call whose LangGraph config carries no thread_id always raises MissingThreadIdError, and so does a Claude Code event without a session_id. PIIGhostClient.detect takes a required thread_id. If your conversations need no separation, name the "default" thread.
# 1.x
middleware = PIIAnonymizationMiddleware(pipeline, require_thread_id=False)
await agent.ainvoke({"messages": messages})
# 2.0
middleware = PIIAnonymizationMiddleware(pipeline)
await agent.ainvoke(
{"messages": messages}, config={"configurable": {"thread_id": "default"}}
)The override lists are renamed
In 1.x the override's whitelist held the values always masked, and its blacklist the values always left in clear. Most readers expect the reverse from these two words, and a reader who inverts them leaves in clear the values they meant to mask. 2.0 takes the names Presidio uses.
| 1.x | 2.0 | Meaning |
|---|---|---|
whitelist, [override.whitelist] | deny_list, [override.deny_list] | values always masked |
blacklist, [override.blacklist] | allow_list, [override.allow_list] | values always left in clear |
whitelist_strategy, WhitelistStrategy | deny_list_strategy, DenyListStrategy | how the deny list treats a value the assistant wrote |
blacklist_strategy, BlacklistStrategy | allow_list_strategy, AllowListStrategy | which detections an allow list hit clears |
whitelist_wins, OverrideConflictStrategy.WHITELIST_WINS | deny_list_wins, OverrideConflictStrategy.DENY_LIST_WINS | a value on both lists is masked |
blacklist_wins, OverrideConflictStrategy.BLACKLIST_WINS | allow_list_wins, OverrideConflictStrategy.ALLOW_LIST_WINS | a value on both lists stays in clear |
The members RESPECT_PROVENANCE, FORCE, EXACT, VALUE, OVERLAP and RAISE keep their names. A config that still uses an old key or value is refused at load time with the name that replaces it, and is never read under the new meaning. DetectionOverride(whitelist=...) raises TypeError.
# 1.x
[override]
whitelist_strategy = "force"
conflict_strategy = "whitelist_wins"
[override.whitelist]
type = "regex"
patterns = { CODENAME = 'ACME-[A-Z]+' }
# 2.0
[override]
deny_list_strategy = "force"
conflict_strategy = "deny_list_wins"
[override.deny_list]
type = "regex"
patterns = { CODENAME = 'ACME-[A-Z]+' }Behaviours that changed
- Unicode spaces.
RegexDetectorreads every Unicode space as an ordinary one, so a pattern that looked for a no-break space on purpose no longer finds one. Two values that differ only by their spaces are one value, and get one token. - Hyphens. In a whole-word search, every Unicode hyphen joins two words, the non-breaking one Word types included. Jean is therefore no longer found inside Jean‑Paul written with that hyphen.
- NER detectors. Every adapter re-reads the text of a detection from the source and applies its threshold itself, whatever its model returns. A
Gliner2Detectordetection can therefore carry a slightly different text than before, the document's rather than the model's. - Overrides. After a deny list, two detections on one span keep their detector order, as they do when no deny list is set.
- In-process memory.
InMemoryConversationMemoryis bounded by default to 10,000 threads and one day idle. An evicted or expired thread no longer restores its tokens. Passmax_threads=Noneandttl=Noneto get the unbounded 1.x store back. - LLM detector and guard. An output
LLMDetectororLLMGuardRailcannot read raisesUnreadableOutputErrorinstead of reading as zero detections, so the message is refused. Passfail_open=True, orfail_open = truein a config, to send it on undetected as 1.x did. - Claude Code hooks. A hook that cannot de-identify, a server down for instance, blocks the prompt or the tool call and replaces a tool output by a notice. Set
PIIGHOST_HOOK_FAIL_OPEN=1to let the text through in clear as 1.x did.
A conversation memory written by 1.x keys the provenance of a value by its casefolded text (lowercased in the Unicode sense), while 2.0 keys it by the value with its spaces collapsed. A value typed with an unusual space can lose its provenance across the upgrade. If that provenance matters to a thread in flight, purge the store, as for the Argon2 change below.
The API server
piighost-api requires piighost>=2.0,<3, and its configuration follows the 2.0 rules above, the catalog references of catalogs included.
--configandPIIGHOST_CONFIGtake a catalog reference as well as a file path.- A configuration without a
[memory]section is served with the in-process memory instead of being refused. Declare aredismemory to share the threads between instances. /v1/anonymize,/v1/anonymize/correctedand/v1/deanonymizerequire athread_id, and answer400without one instead of using the shared"default"thread./v1/labelsreads the labels of a catalog group from the catalog.- Observation goes through the standard
OTEL_*variables.LANGFUSE_PUBLIC_KEYandLANGFUSE_SECRET_KEYservedataset extractonly, and noOPIK_*variable is read. - The server reads no
REDIS_URL. The Redis address is theurlof the[memory]section. - A deployment still setting
PIPELINE_PATH, or passing amodule:variablepath, predates the TOML loader. SetPIIGHOST_CONFIGto a config file or a catalog reference instead.
The routes and the variables are listed in API endpoints and Server CLI.
Argon2 digests changed
Argon2Hasher now runs the value through HMAC-SHA256, keyed by the pepper (the secret read from PIIGHOST_HASH_PEPPER), before Argon2id hashes it. A digest is therefore no longer the one an earlier release produced. Nothing in the API changed, but every key already stored under the old digest becomes unreachable.
This concerns a Redis or SQLAlchemy conversation memory built with type = "argon2". A deployment on sha256, or with no hasher at all, is unaffected.
Stored entries are orphaned rather than corrupted. The pipeline finds nothing under the new key, treats the message as never seen, and re-detects it. An in-flight thread therefore restarts its token numbering, and the same value can land on a different number than the one the model has been reading.
Purge the store as part of the upgrade, before restarting the application:
# Redis, the whole database backing the conversation memory
redis-cli -n 0 FLUSHDB-- SQLAlchemy, the conversation memory table (its default name)
TRUNCATE TABLE piighost_conversation_messages;An entry left behind expires on its own when a ttl is configured. Without a ttl, it stays forever, so purging is the only way to reclaim the space.
Coming from 0.x
A 0.x code base is ported by rewriting its setup rather than by renaming imports, because every release before 1.0.0 exposed a different API. What changed:
- imports live under
piighost.components,piighost.pipeline,piighost.configandpiighost.integrations - a pipeline takes a detector and nothing else, since the linker and the anonymizer have defaults
- the
faker,cache,langfuseandopikextras are gone, and no stage caches detection results - the
sqlalchemyextra came back in 1.2.0, as a conversation memory backend
Restart from the Quickstart, then read First pipeline for the stages and the configuration reference to move the setup into a config file.