Examples¶
Short recipes to copy. Each client below talks to Privyx instead of the provider: only the base URL changes, and the client keeps its own API key, which Privyx relays. The examples assume a proxy on the default port:
privyx proxy --upstream https://api.openai.com # for the OpenAI examples
privyx proxy --upstream https://api.anthropic.com # for the Anthropic examples
For a guided walkthrough instead, see the tutorials.
OpenAI Python SDK¶
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1") # the key still comes from OPENAI_API_KEY
reply = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Write a short greeting to alice@example.com"}],
)
print(reply.choices[0].message.content)
The provider receives <PRIVYX_EMAIL_1> in place of the address; the reply
has the address back. Streaming (stream=True) is restored as it arrives.
Setting OPENAI_BASE_URL=http://localhost:8000/v1 has the same effect without
touching the code.
Anthropic Python SDK¶
import anthropic
client = anthropic.Anthropic(base_url="http://localhost:8000") # the key still comes from ANTHROPIC_API_KEY
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Write a short greeting to alice@example.com"}],
)
print(next(block.text for block in reply.content if block.type == "text"))
Or set ANTHROPIC_BASE_URL=http://localhost:8000.
Node¶
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "http://localhost:8000/v1" }); // the key still comes from OPENAI_API_KEY
const reply = await client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Write a short greeting to alice@example.com" }],
});
console.log(reply.choices[0].message.content);
The Anthropic SDK for Node takes baseURL: "http://localhost:8000" in the
same way. Streaming, tool calls, and the Responses API are in
An app on the OpenAI or Anthropic SDK; LangChain,
LlamaIndex, LiteLLM, and the Vercel AI SDK are under
Integrations.
One session per user¶
By default every request gets its own session. To keep one mapping per user
of your application, send an x-privyx-session header:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
default_headers={"x-privyx-session": "user-42"},
)
anthropic.Anthropic takes the same default_headers argument. Privyx strips
the header before forwarding and returns the session id in the response's
x-privyx-session header. A persistent vault keeps the mapping across
restarts; see Sessions and vault.
curl¶
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Write a short greeting to alice@example.com"}]}'
curl http://localhost:8000/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "claude-opus-5-5", "max_tokens": 1024,
"messages": [{"role": "user", "content": "Write a short greeting to alice@example.com"}]}'
Privyx adds the anthropic-version header when the request has none.
Coding tools¶
privyx run starts a proxy for the tool and points it there:
privyx run claude # Claude Code
privyx run codex # Codex
privyx run aider # aider
For a long-running proxy that several terminals share, start it once and set the variable yourself:
privyx proxy -c privyx.yaml --upstream https://api.anthropic.com
ANTHROPIC_BASE_URL=http://localhost:8000 claude
On the command line¶
# What would be masked in this text, and what the provider would get
privyx detect --transform "DB_PASSWORD=hunter2, mail dana@acme.example"
# The same for a file
privyx detect --transform --stdin < app.log
# Mask a file and keep the mapping; restore the answer later
privyx mask --map map.json -i app.log -o app.masked.log
privyx unmask --map map.json -i answer.txt
# Only the messages of a JSON request
privyx mask --map map.json -i request.json --path '$.messages'
# A JSONL dataset through a pipe
cat tickets.jsonl | privyx mask --map map.json -f jsonl -i - -o tickets.masked.jsonl
# What happened lately, without any content
privyx audit stats --since 24h
Configuration recipes¶
These files ship in the repository's
configs/ folder and in the
Docker image under /app/configs.
Your own word lists¶
# Example: pseudonymize your own list of literal terms
#
# `terms` takes plain strings — Privyx escapes them, orders the longest first so
# a compound term wins over a substring of itself, and adds word boundaries.
# Matching is case-insensitive. The entity name is yours to choose; it is the
# {type} in the emitted token, e.g. <PRIVYX_PERSON_1>.
#
# Every value below is a placeholder — replace it with your own.
detector:
type: regex # keeps the built-in patterns (personal data and secrets)
terms:
PERSON: [ann, bob, annbob]
ORGANIZATION: [acme, initech]
URL: ["https://git.internal.example/team"]
PROJECT: [bluebird] # any entity name works
# `terms` and `patterns` are merged, so hand-written regexes still work.
patterns:
EMPLOYEE_ID: "\\bEMP-\\d{6}\\b"
operator:
type: pseudonym
vault:
type: memory
# With `policy.type: strict`, every entity above would also need to appear in
# `policy.allowed` — the default policy passes everything through.
policy:
type: default
No plain text in the vault¶
# Example: encrypt operator — no plaintext PII at rest in the vault
#
# The vault stores the AES-256-GCM ciphertext of each detected value (not the
# original) and Privyx decrypts on the way back. Requires the `crypto` extra:
# pip install privyx[crypto]
#
# Provide the key via the environment (preferred) rather than committing it:
# export PRIVYX_ENCRYPT_KEY=$(openssl rand -hex 32)
operator:
type: encrypt
key: "" # 64 hex chars; set via PRIVYX_ENCRYPT_KEY
vault:
type: memory
A stricter deployment¶
# Privyx strict configuration
#
# Stricter defaults for higher-sensitivity deployments.
host: 127.0.0.1
port: 8000
log_level: info
# Empty → falls through to provider.base_url below.
upstream_url: ""
vault:
type: sqlite # persist sessions across restarts
dsn: sqlite+aiosqlite:///privyx.db
# `regex` patterns are layered over the built-in set, so only the additions
# need listing here.
detector:
type: regex
patterns:
DATE_OF_BIRTH: "\\b\\d{4}-\\d{2}-\\d{2}\\b"
policy:
type: strict
allowed: [EMAIL, PHONE, SSN, CREDIT_CARD, IP_ADDRESS, DATE_OF_BIRTH,
API_KEY, JWT, PRIVATE_KEY, AUTH_TOKEN, URL_CREDENTIAL, SECRET]
operator:
type: pseudonym
# Empty secret → per-session counters. Set one (or PRIVYX_ANCHOR_SECRET) to
# make pseudonyms deterministic across sessions.
anchor:
type: hmac
secret: ""
provider:
type: generic
base_url: http://localhost:20128
api_key: ""
headers: {}
# Keep the PII-safe audit trail on for higher-sensitivity deployments.
audit:
enabled: true
# path defaults to privyx-audit.log (privyx run: ~/.local/state/privyx/audit.log)
Sessions that survive a restart¶
vault:
type: sqlite # needs privyx[sqlite]
dsn: /var/lib/privyx/privyx.db
ttl: 604800 # forget sessions idle for a week
session:
strategy: conversation # one session per conversation
Refuse what cannot be masked¶
proxy:
passthrough_unknown: false # 403 for embeddings and every other unrouted path
Names without a list¶
detector:
- type: regex # built-in patterns, your terms and patterns
- type: presidio # needs privyx[presidio] and a spaCy model
language: en
entities: [PERSON, LOCATION]
Using the engine from Python¶
The engine can run inside your own program, without the proxy. The scripts in
examples/ show how,
and the test suite runs each of them. Python library
walks through them.
"""Build the engine from settings, the way `privyx proxy` does.
Instead of wiring a detector, a policy, an operator, and a vault by hand, load
the same configuration the CLI reads and let Privyx assemble them.
"""
from __future__ import annotations
import asyncio
from privyx.config.loader import load_config
from privyx.core.builder import build_engine
async def main() -> None:
# The same layers as the CLI: built-in defaults, a YAML file (pass its
# path as the first argument), PRIVYX_* environment variables, then `extra`.
settings = load_config(extra={"detector": {"terms": {"PROJECT": ["bluebird"]}}})
engine, close = await build_engine(settings)
try:
session = await engine.get_or_create_session()
masked = await engine.transform("bluebird ships to dana@acme.example", session=session)
print(masked.text) # <PRIVYX_PROJECT_1> ships to <PRIVYX_EMAIL_2>
reply = f"Noted: {masked.text}" # stands in for a model's answer
restored = await engine.restore(reply, session.session_id)
print(restored.text) # Noted: bluebird ships to dana@acme.example
finally:
await close() # releases the vault
if __name__ == "__main__":
asyncio.run(main())