maskflow-gateway
A drop-in OpenAI / Anthropic-compatible proxy that detects PII in a request, replaces it with reversible typed placeholders before the request reaches the provider, and restores the originals in the response — including mid-stream.
Point your existing client's base URL at the gateway. No SDK change, no code change.
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1", # <- the gateway
api_key="sk-...", # <- your real OpenAI key, passed straight through
)
client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Email Rahul at rahul.sharma@example.com about PAN ABCPE1234F"}
],
)
# OpenAI sees: "Email Rahul at <EMAIL_1> about PAN <PAN_1>"
# You get back: the model's reply with <EMAIL_1>/<PAN_1> restored to the real values
Endpoints
| Route | What it does |
|---|---|
POST /v1/chat/completions |
OpenAI chat, streaming + non-streaming, tool calls |
POST /v1/messages |
Anthropic Messages, streaming + non-streaming, tool use |
POST /v1/embeddings |
masks each input before it is embedded — the RAG path |
POST /v1/mask / POST /v1/unmask |
direct masking, no upstream call |
GET /healthz |
liveness (never touches Redis) |
GET /readyz |
readiness — 503 fail-closed if Redis maxmemory-policy != noeviction |
GET /metrics |
Prometheus |
GET /v1/entities |
every PII type the loaded packs detect |
Streaming unmask
The model streams the reply in arbitrary chunks and a placeholder like
<PERSON_NAME_1> can be split across two SSE frames. The gateway parses
the provider's SSE, keeps a rolling buffer + a trie of the session's active
placeholders, and emits the longest prefix that is certain — a completed
placeholder (replaced) or a character that cannot begin any placeholder —
retaining only the tail that could still grow into one. A two-layer
decoder handles chunk splits mid-UTF-8. Property: for any chunking of a
masked reply, the concatenated stream equals the non-streaming
unmask result (fuzz-tested at every byte boundary).
Sessions (multi-turn / tool loops)
Send X-Maskflow-Session: <your opaque id> to keep <PHONE_1> meaning the
same number across every turn and tool call of one agent run. Without the
header each request is masked and unmasked in isolation.
X-Maskflow-Session-TTL: <seconds>overrides the default (3600), capped atMASKFLOW_GATEWAY_SESSION_TTL_MAX_SECONDS(86400).- Keyed sessions need Redis (
MASKFLOW_GATEWAY_REDIS_URL). Mappings are encrypted with AES-256-GCM (MASKFLOW_GATEWAY_SESSION_KEY, 32 random bytes hex) before they touch Redis, with a mandatory TTL. - Redis must run
maxmemory-policy noeviction— an evicted session mid-conversation means unmask finds nothing and a user sees raw<PERSON_NAME_1>text./readyzreturns 503 until this is fixed.
Configuration
Every setting is an environment variable prefixed MASKFLOW_GATEWAY_:
| Variable | Default | Notes |
|---|---|---|
OPENAI_BASE_URL |
https://api.openai.com/v1 |
must include the version segment |
ANTHROPIC_BASE_URL |
https://api.anthropic.com/v1 |
|
UPSTREAM_API_KEY |
(unset) | set → gateway injects it; unset → client's own key is forwarded, nothing stored |
NER |
0 |
1 enables the spaCy pass (bare Indian names & addresses); much slower — see loadtest/ |
MIN_CONFIDENCE |
0.5 |
detection threshold |
REDIS_URL |
(unset) | unset → in-process ephemeral sessions only (single replica) |
SESSION_KEY |
(unset) | hex, 32 bytes; required when REDIS_URL is set |
SESSION_TTL_SECONDS / SESSION_TTL_MAX_SECONDS |
3600 / 86400 |
|
REQUIRE_MAXMEMORY_NOEVICTION |
true |
|
MAX_REQUEST_BYTES |
2000000 |
|
UPSTREAM_TIMEOUT_SECONDS / UPSTREAM_CONNECT_TIMEOUT_SECONDS |
120 / 10 |
|
RATE_LIMIT_PER_MINUTE / RATE_LIMIT_BURST |
0 (off) |
keyed by a hash of the client's Authorization |
TOOL_CALL_MAX_DEPTH / TOOL_CALL_MAX_ITEMS |
32 / 10000 |
bound the tool-argument JSON walk |
JSON_LOGS |
1 |
structured logs; every record also passes MaskFlow's PII scrub filter |
CORS_ALLOW_ORIGINS |
[] |
JSON list |
The gateway's masking config comes only from these variables — it does not
read a .maskflowrc.
Run
pip install "maskflow-gateway[redis]"
export MASKFLOW_GATEWAY_REDIS_URL=redis://localhost:6379/0
export MASKFLOW_GATEWAY_SESSION_KEY=$(python -c "import os;print(os.urandom(32).hex())")
maskflow-gateway --host 0.0.0.0 --port 8000 --workers 4
Deploy artifacts in deploy/: multi-arch Dockerfile,
docker-compose.yml (gateway + noeviction Redis), a Helm chart
(deploy/helm/maskflow-gateway, with HPA / PDB / ServiceMonitor), and
Fly / Render / Railway templates.
Throughput
Published honestly, with the hardware, in loadtest/README.md.
Rough laptop floor: ~430 req/s pattern-only, ~90 req/s NER-enabled
(4 workers, Intel i7-9750H).
License
MIT — like the rest of MaskFlow. No license gates, no paid flags, no telemetry.
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 maskflow_gateway-0.2.0.tar.gz.
File metadata
- Download URL: maskflow_gateway-0.2.0.tar.gz
- Upload date:
- Size: 43.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16fbbcb266847f35c69b6631a3ed053635a54588f494ad86911ec8c99ab86a8b
|
|
| MD5 |
c442afac3bb9166c2fdea136d5d4fe80
|
|
| BLAKE2b-256 |
b5eea7b37e24466490a6c820024ea74cd5e34453c623709835ba0b72cfa4fc78
|
Provenance
The following attestation bundles were made for maskflow_gateway-0.2.0.tar.gz:
Publisher:
release-gateway.yml on maskflow/maskflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maskflow_gateway-0.2.0.tar.gz -
Subject digest:
16fbbcb266847f35c69b6631a3ed053635a54588f494ad86911ec8c99ab86a8b - Sigstore transparency entry: 2761345257
- Sigstore integration time:
-
Permalink:
maskflow/maskflow@6ab4e25046e827ed7d468936eb80b4f4772c197d -
Branch / Tag:
refs/tags/gateway-v0.2.0 - Owner: https://github.com/maskflow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-gateway.yml@6ab4e25046e827ed7d468936eb80b4f4772c197d -
Trigger Event:
push
-
Statement type:
File details
Details for the file maskflow_gateway-0.2.0-py3-none-any.whl.
File metadata
- Download URL: maskflow_gateway-0.2.0-py3-none-any.whl
- Upload date:
- Size: 36.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ae0893bb75b37ee331522f8293d5de79d7adc9ae601f86a164c6e617ad572a4
|
|
| MD5 |
350030e16aa42859ab0ebd6b12615653
|
|
| BLAKE2b-256 |
0b0274829535d5f39b34a3bc470f3ff71aa4a1a3f8a873c3882257a044bd562a
|
Provenance
The following attestation bundles were made for maskflow_gateway-0.2.0-py3-none-any.whl:
Publisher:
release-gateway.yml on maskflow/maskflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maskflow_gateway-0.2.0-py3-none-any.whl -
Subject digest:
1ae0893bb75b37ee331522f8293d5de79d7adc9ae601f86a164c6e617ad572a4 - Sigstore transparency entry: 2761345264
- Sigstore integration time:
-
Permalink:
maskflow/maskflow@6ab4e25046e827ed7d468936eb80b4f4772c197d -
Branch / Tag:
refs/tags/gateway-v0.2.0 - Owner: https://github.com/maskflow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-gateway.yml@6ab4e25046e827ed7d468936eb80b4f4772c197d -
Trigger Event:
push
-
Statement type: