Skip to content
Sırdaş

Sırdaş Gateway: redact PII before your app calls an LLM

Redact every OpenAI or Anthropic request on your machine.

For your AI agent

Paste this into Claude, ChatGPT or Cursor and the agent installs and configures itself by reading the Markdown version of this page.

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 containsWhyWhat to do
An image or audioIt cannot be redactedRemove it, or set SIRDAS_GATEWAY_ALLOW_IMAGES=1 to pass it through unredacted, deliberately
A scanned PDFNo text layer to redactRun OCR locally first (sirdas ocr)
A PDF it cannot openUsually password-protectedsirdas unlock with the password you know
An unknown routeUnredacted trafficUse 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

VariableDefault
SIRDAS_GATEWAY_PORT8787
SIRDAS_GATEWAY_HOST127.0.0.10.0.0.0 would let anyone on the network use your key through it
OPENAI_UPSTREAM, ANTHROPIC_UPSTREAMofficial APIsAzure OpenAI, another proxy, a local model
SIRDAS_LABELSenes for [NOMBRE_1], auto to follow the text
SIRDAS_GATEWAY_POLICYnonePer-key rules, see below
SIRDAS_GATEWAY_QUIEToffSilence 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…" }
  ]
}
FieldEffect
upstreamsopenai, anthropic, or both (default)
modelsAllowed models; a trailing * is a prefix
blockPlaceholder types that make the request fail with 422 instead of leaving redacted (SSN, MEDICAL_RECORD, PERSON, …)
allowImagesPer-key override of SIRDAS_GATEWAY_ALLOW_IMAGES
defaultRule 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_UPSTREAM accepts 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.