privedge (Python SDK)
Privacy on the edge. Python client for the Privedge compliance proxy — a drop-in-style AI inference gateway that detects PII/secrets in your prompts and either anonymizes them before forwarding to a cloud LLM, routes inference to a Cloudflare edge model instead (no external call at all), or passes clean prompts through untouched.
Functional parity with @privedge/sdk (TypeScript). See the root
README for how the proxy itself works.
Install
pip install privedge
Requires Python 3.9+. The only runtime dependency is httpx (see Dependencies).
Quickstart — sync
from privedge import Privedge
client = Privedge(api_key="pvdg_live_...", worker_url="https://edge.privedge.io")
resp = client.chat.completions.create(
model="gpt-5.4-nano",
messages=[{"role": "user", "content": "Summarize this patient record..."}],
)
print(resp.choices[0].message["content"])
print(resp.routed_to) # "edge" | "cloud"
print(resp.anonymized) # True if PII was redacted before the cloud call
print(resp.pii_matches) # number of PII tokens detected
print(resp.ner_ran) # whether semantic (NER) detection ran — see below
Or as a context manager, to close the HTTP session automatically:
with Privedge(api_key="pvdg_live_...", worker_url="https://edge.privedge.io") as client:
resp = client.chat.completions.create(
model="gpt-5.4-nano",
messages=[{"role": "user", "content": "..."}],
)
Quickstart — async
import asyncio
from privedge import AsyncPrivedge
async def main():
async with AsyncPrivedge(api_key="pvdg_live_...", worker_url="https://edge.privedge.io") as client:
resp = await client.chat.completions.create(
model="gpt-5.4-nano",
messages=[{"role": "user", "content": "..."}],
)
print(resp.choices[0].message["content"])
asyncio.run(main())
Other endpoints
# Dry-run PII/secret detection, no LLM call
detect = client.detect(messages=[{"role": "user", "content": "My DNI is 12345678Z"}])
print(detect.pii_types, detect.pii_count, detect.has_secret, detect.ner_ran)
# API key metadata
info = client.keys.info()
print(info.provider, info.provider_mode, info.cloud_model)
ner_ran — read this before you rely on empty PII results
Every response — ChatCompletionResponse and DetectResponse — carries a
boolean ner_ran field.
The semantic detection layer (Named Entity Recognition — catches person
names, organizations, and street addresses that regex cannot) is gated
behind the Pro/Enterprise tier. On the Free tier it never runs, so
ner_entities (or pii_types restricted to NER-only categories) is
always empty — not because your prompt is clean, but because names,
organizations and addresses were never looked for.
resp = client.chat.completions.create(model="gpt-5.4-nano", messages=[...])
if not resp.ner_ran:
# resp.ner_entities == [] here tells you NOTHING about whether the
# prompt contains a person's name, a hospital name, or a street address.
# Only regex-coverable PII (SSN, DNI, email, credit cards, phone, etc.)
# was checked.
...
Always check ner_ran before treating an empty ner_entities (or the
absence of NER-only types in pii_types) as "this prompt has no PII." Treat
it as "no PII of the types actually checked."
Response fields
ChatCompletionResponse
| Field | Type | Meaning |
|---|---|---|
id |
str |
Completion id (provider-assigned, or chatcmpl-edge-<ts> for edge routing) |
object |
str |
Always "chat.completion" |
model |
str |
Model that actually served the request |
choices |
list[ChatCompletionChoice] |
OpenAI-compatible choices, each with index, message, finish_reason |
routed_to |
"edge" | "cloud" |
Where inference actually ran |
anonymized |
bool |
Whether PII was redacted before the cloud call |
pii_matches |
int |
Total PII tokens matched (regex + NER combined) |
ner_entities |
list[str] |
Entity types found by NER (e.g. ["PERSON", "ORG"]) — always [] when ner_ran is False |
ner_ran |
bool |
Whether the semantic (NER) layer ran — see above |
latency_ms |
int |
End-to-end latency measured by the worker |
applied_strategy |
"anonymize" | "edge" |
Strategy actually applied to this request |
strategy_mode |
"anonymize" | "edge" | "custom" |
The API key's configured mode |
raw |
dict |
Unparsed JSON body, for fields not yet modeled |
DetectResponse (from client.detect(...))
| Field | Type | Meaning |
|---|---|---|
pii_types |
list[str] |
Distinct PII types found |
pii_type_counts |
dict[str, int] |
Count per type |
pii_count |
int |
Total count across all types |
has_secret |
bool |
Whether a secret pattern (API key, JWT, DB connection string) was found |
sanitized_messages |
list[Message] |
Messages with PII replaced by [ANON_<TYPE>_<n>] tokens |
ner_ran |
bool |
Same caveat as above |
ner_error |
bool | None |
True if NER was attempted (paid tier) but failed. Not blocking — unlike chat.completions.create, this endpoint returns 200 either way; check this field explicitly |
KeyInfo (from client.keys.info())
| Field | Type | Meaning |
|---|---|---|
provider |
str |
e.g. "openai", "anthropic" |
provider_mode |
str |
"managed" (Privedge's own provider keys) or "byok" |
cloud_model |
str | None |
Default cloud model configured for this key |
Exceptions
All errors raised by the SDK derive from PrivedgeError, which carries
.message, .status_code, and .body (the parsed JSON error payload, if
any).
| Exception | When | Notes |
|---|---|---|
PrivedgeAuthError |
HTTP 401 | Missing or invalid API key |
PrivedgeRateLimitError |
HTTP 429 | Also exposes .limit, .remaining, .reset, .edge_limit, .edge_remaining from the X-RateLimit-* / X-Edge-* response headers |
PrivedgeStrategyMismatchError |
HTTP 400, code: "strategy_mismatch" |
A request-level pii_strategy conflicts with a key fixed to edge/anonymize. Also exposes .configured. This is a config error — fix your key or drop the field, don't retry blindly |
PrivedgeNERUnavailableError |
HTTP 503, code: "ner_unavailable" |
Semantic detection failed on a Pro/Enterprise key using the anonymize strategy, and the worker refused to forward the prompt without a PII guarantee. No data was sent to the cloud provider. Safe and expected to retry with backoff |
PrivedgeAPIError |
Any other non-2xx | Includes upstream provider errors and edge-inference failures (502) |
from privedge import Privedge, PrivedgeNERUnavailableError, PrivedgeRateLimitError
with Privedge(api_key="...", worker_url="...") as client:
try:
resp = client.chat.completions.create(model="gpt-5.4-nano", messages=[...])
except PrivedgeNERUnavailableError:
# Nothing leaked — detection failed closed. Retry shortly.
...
except PrivedgeRateLimitError as e:
print(f"retry after reset={e.reset}, remaining={e.remaining}")
Detection scope by tier
| Layer | Free | Pro / Enterprise |
|---|---|---|
| Regex (deterministic: SSN, credit cards, IBAN, DNI/NIE, email, phone, dates, medical IDs, secrets) | Yes | Yes |
NER — person names, organizations, street addresses (Workers AI llama-3.3-70b) |
No | Yes |
On Free, client.detect(...) and chat.completions.create(...) only ever
check the deterministic regex layer. If your prompts might contain names of
patients, clients, or organizations and that matters for your compliance
posture, you need a Pro/Enterprise key — ner_ran=False on Free is not a
bug, it's the tier boundary made visible instead of silently assumed.
Model resolution
model is optional on chat.completions.create(...):
# model omitted — the worker falls back to the cloud_model configured on
# the API key (see client.keys.info().cloud_model)
resp = client.chat.completions.create(
messages=[{"role": "user", "content": "..."}],
)
# model passed — always wins over the key's configured default
resp = client.chat.completions.create(
model="gpt-5.4-nano",
messages=[{"role": "user", "content": "..."}],
)
Passing model always takes precedence over the key's configured
cloud_model. This keeps the SDK a drop-in OpenAI replacement, and on a
self-hosted deployment — where there's no dashboard to configure a key's
default model — it's the only way to choose one at all.
When model is omitted, the SDK does not send the field (never as
model: null); the worker only honors a request-supplied model when it's
present as a string.
No streaming
The Privedge worker has no streaming code path — stream is not accepted
by this SDK. Every request is a single buffered request/response.
Timeouts
client = Privedge(api_key="...", worker_url="...", timeout=30.0) # seconds, default 60.0
Applies to connect/read/write/pool via httpx's timeout model. A PrivedgeAPIError is raised on timeout.
Dependencies
| Client | Library | Why |
|---|---|---|
Sync (Privedge) |
httpx |
Typed timeouts, connection pooling, and — critically — an API that is structurally identical to its async counterpart. Standard-library urllib would drop the dependency count to zero, but at the cost of hand-rolling JSON error bodies, header parsing, and timeout handling twice, once for each client, with no code sharing possible. One well-maintained dependency shared by both clients beats zero dependencies duplicated by hand. |
Async (AsyncPrivedge) |
httpx (AsyncClient) |
Same library as sync, so the two clients share one dependency instead of two (httpx + aiohttp), and their code paths (_post/_get, error mapping in _errors.py) stay in lockstep by construction rather than by discipline. |
Total runtime dependency footprint: one package (httpx).
Development
cd packages/sdk-python
pip install -e ".[dev]"
pytest
Tests mock all HTTP calls via respx — no network access required, no live worker needed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file privedge-1.0.0.tar.gz.
File metadata
- Download URL: privedge-1.0.0.tar.gz
- Upload date:
- Size: 16.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96f4ad015f2bbc1d2a7d9d19064d55cc10aa089b02386c28984a044830702445
|
|
| MD5 |
78a2af14bdef92ae943d690e32661160
|
|
| BLAKE2b-256 |
3eeb080e5670330fd53a237e2485254e60a99522ff814d771f22dac062012d32
|
File details
Details for the file privedge-1.0.0-py3-none-any.whl.
File metadata
- Download URL: privedge-1.0.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
692d3797372c6870f6c95d48b80ef5fbcd2cc424459bb060dc20e01ccdcbfc42
|
|
| MD5 |
ea32042dd6ae898c4788925bf4f43ee6
|
|
| BLAKE2b-256 |
af70844416c4327a11c15f6d6f67b8b56d6fee1d5731d21785a77575b5c1c970
|