Since 0.5.0. npx -y @sirdaspdf/gateway@0.5.0
A local proxy that sits between your application and the OpenAI or Anthropic API. It replaces names, SSNs, MRNs, addresses, emails, account and card numbers with placeholders such as [PERSON_1] on your machine, forwards the request with your own API key, and puts the real values back into the answer.
your app ──► Sırdaş Gateway (127.0.0.1) ──► OpenAI / Anthropic
redacts the request sees [PERSON_1], [SSN_1]
your app ◄── restores the real values ◄────── answers with placeholders
What changes in your code
One line: the base URL. Your key, your model and your prompts stay as they are.
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/openai/v1")
from anthropic import Anthropic
client = Anthropic(base_url="http://127.0.0.1:8787/anthropic")
Supported: OpenAI chat/completions, responses and embeddings; Anthropic messages. Streaming works on all of them: a placeholder split across two chunks ([PERS + ON_1]) is held back until it is complete.
In-process: middleware for the Vercel AI SDK
No separate process: wrapLanguageModel redacts the prompt in your own process and restores the real values in the answer, streaming included.
import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { sirdasMiddleware } from "@sirdaspdf/gateway/ai-sdk";
const model = wrapLanguageModel({
model: openai("gpt-5"),
middleware: sirdasMiddleware({ block: ["SSN"] }),
});
const { text } = await generateText({ model, prompt: "..." });
Text, reasoning, tool calls and tool results are redacted with one map per call. An attached file cannot be redacted here: it is rejected before the call (files: "pass" lets it through untouched; convert PDFs to text with sirdas markdown instead). block refuses the call when a type appears, even redacted. onRedact receives counts by type, never values. Tested against ai@7 (default, specificationVersion: "v4") and ai@6 (specificationVersion: "v3") with their mock models.
Attached PDFs
An inline PDF (OpenAI file part, Anthropic document block) is converted to text on your machine and redacted with the rest of the request. The PDF bytes never reach the provider.
What it refuses, and why
A privacy gateway that forwards what it cannot read is not one. These return 422 and nothing is sent:
| Request contains | Why | What to do |
|---|---|---|
| An image or audio | It cannot be redacted | Remove it, or set SIRDAS_GATEWAY_ALLOW_IMAGES=1 to pass it through unredacted, deliberately |
| A scanned PDF | No text layer to redact | Run OCR locally first (sirdas ocr) |
| A PDF it cannot open | Usually password-protected | sirdas unlock with the password you know |
| An unknown route | Unredacted traffic | Use one of the supported routes |
What is logged
One JSON line per request on stderr: route, model, status, time and the count of each placeholder type. Never a value. Every response carries x-sirdas-redacted: 4 and x-sirdas-redacted-types: PERSON:1,SSN:1,….
Configuration
| Variable | Default | |
|---|---|---|
SIRDAS_GATEWAY_PORT | 8787 | |
SIRDAS_GATEWAY_HOST | 127.0.0.1 | 0.0.0.0 would let anyone on the network use your key through it |
OPENAI_UPSTREAM, ANTHROPIC_UPSTREAM | official APIs | Azure OpenAI, another proxy, a local model |
SIRDAS_LABELS | en | es for [NOMBRE_1], auto to follow the text |
SIRDAS_GATEWAY_POLICY | none | Per-key rules, see below |
SIRDAS_GATEWAY_QUIET | off | Silence the per-request log line |
A Docker image (ghcr.io/brayan15p/sirdas-gateway) ships with the release; run it with -p 127.0.0.1:8787:8787 so the port is only reachable from the host.
Per-key policy
One gateway, several applications, different rules. SIRDAS_GATEWAY_POLICY points at a JSON file that says what each API key may do. Keys are listed by their sha256, never in clear: the file is safe to commit and review.
{
"version": 1,
"name": "acme",
"keys": [
{ "name": "support bot", "sha256": "3f1c…", "upstreams": ["anthropic"], "models": ["claude-haiku*"], "block": ["SSN", "MEDICAL_RECORD"] },
{ "name": "underwriting", "sha256": "9ab0…" }
]
}
| Field | Effect |
|---|---|
upstreams | openai, anthropic, or both (default) |
models | Allowed models; a trailing * is a prefix |
block | Placeholder types that make the request fail with 422 instead of leaving redacted (SSN, MEDICAL_RECORD, PERSON, …) |
allowImages | Per-key override of SIRDAS_GATEWAY_ALLOW_IMAGES |
default | Rule for keys not listed. Without it, unlisted keys get 403 |
printf %s "$ANTHROPIC_API_KEY" | sirdas-gateway hash-key
A malformed policy (unknown field, unknown type, a key pasted in clear) stops the gateway from starting: it never runs with fewer rules than you wrote. A refused request is not forwarded, and the log line names the rule, not the key.
Deploy it on your infrastructure
deploy/docker-compose.yml (in the package and the repository) runs the image with the port published only to the host, the policy mounted read-only, a read-only container filesystem and no extra privileges. A test fails if any of those lines changes.
cp policy.example.json policy.json # one entry per API key, by sha256
printf %s "$OPENAI_API_KEY" | docker run --rm -i ghcr.io/brayan15p/sirdas-gateway:0.5.0 hash-key
docker compose up -d
The gateway is a control only when it is the only way out: keep the real API keys in the apps that go through it, and block direct egress to the providers at your network.
Limits, stated before you find them
- Redaction is rule-based. It finds labelled names («Patient Name:», «Account holder:») and every later mention of them, but misses a name that never appears next to a label.
- Against the 18 HIPAA Safe Harbor identifiers it covers 16 fully or partly; it does not handle biometric identifiers or face photos, and several are only caught next to a label (names, licences, other unique numbers). It is not a HIPAA compliance certification, and Sırdaş holds no SOC 2 or ISO 27001 certification.
- The model still sees the structure of the document with placeholders. If the document itself is the secret, the right answer is a local model:
OPENAI_UPSTREAMaccepts any compatible endpoint. - This is the only Sırdaş package that uses the network, and only towards the upstream you configure. The CLI and the MCP server never do.