PII Pseudonymizer

LLM gateway

The gateway is not enabled on this server. It is part of the business plan and of every self-hosted installation.

The gateway sits between your application and the AI provider. It behaves exactly like the OpenAI API, so you only change the base URL. For every request it replaces names, addresses, ID numbers and other personal data with realistic fake values, sends the request on, and puts the real values back into the answer. The provider never sees the real data.

  1. Your app sends “Write to Janez Novak …”
  2. The provider receives “Write to Klemen Mlakar …”
  3. The provider answers “Dear Mr Mlakar …”
  4. Your app receives “Dear Mr Novak …”

Quick start

Use any OpenAI SDK. Base URL: http://pii.kwizmo.eu/gateway.php/v1 (once enabled). On Apache the shorter http://pii.kwizmo.eu/v1 works too.

Python
from openai import OpenAI

client = OpenAI(
    base_url="http://pii.kwizmo.eu/gateway.php/v1",   # the only change
    api_key="pii_…",                      # your Kwizmo business key
)

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content":
        "Write a short reply to Janez Novak (janez.novak@gmail.com) about his contract."}],
)
print(reply.choices[0].message.content)   # contains the real name and address again

Keys

Every request needs a Kwizmo API key with the gateway permission (see business plans). There are two ways to pay for the AI model:

The two keys of a gateway request Your application sends the Kwizmo key and, optionally, your own AI provider key to the gateway. The gateway checks the Kwizmo key and keeps it; it replaces the personal data and sends the protected text onwards with the provider key. The AI provider never receives the Kwizmo key or the real data. Your application pii_… Kwizmo key (always) sk-… AI provider key (optional) Kwizmo gateway checks your key · counts your quota replaces the personal data goes no further sk-… yours, or the operator’s protected text, fake names only AI provider OpenAI, Mistral, a local model …
The Kwizmo key stops at the gateway. The AI provider never sees it, and never sees your real data.
  • Provider key stored by the operator: send only your Kwizmo key, Authorization: Bearer pii_….
  • Your own provider key: send it as usual in Authorization and the Kwizmo key in X-PII-Key. It is passed on and never stored.
Python, with your own provider key
client = OpenAI(
    base_url="http://pii.kwizmo.eu/gateway.php/v1",
    api_key="sk-…",                                   # your own provider key, passed on unchanged
    default_headers={"X-PII-Key": "pii_…"},           # your Kwizmo key
)

Supported endpoints

EndpointWhat happens
POST /v1/chat/completionsAll messages are protected (text, content parts, tool calls and tool results). The answer is restored, including streamed answers and tool call arguments.
POST /v1/embeddingsThe input texts are protected before they are embedded.
GET /v1/modelsModels of all configured providers.

Images and files inside messages are passed on unchanged: the gateway cannot read text in pictures.

Options

Send options as HTTP headers, or in a "pii" object in the request body (the object is removed before forwarding). All are optional.

Headerpii fieldMeaning
X-PII-LocalelocaleLanguage of the texts: auto, en, de, at, ch, sl, hr, sr, bs, me, da, no, sv, fi, is. Default: auto.
X-PII-DisabledisableDetectors to skip, comma-separated in the header: name, ml_name, address, postcode, phone, email, iban, national_id, credit_card, date_of_birth, account, secret, ip, ssn.
X-PII-Known-Namesknown_namesJSON array of names to always replace, e.g. your customers.
X-PII-Consistency-KeykeySame key → same fake values in every request, useful for long conversations sent in parts.
X-PII-Restorerestorefalse returns the answer with the fake values.
X-PII-Return-Mappingreturn_mappingtrue adds "pii": {"replacements", "mapping"} to non-streamed answers.
X-PII-Instructioninstructionfalse skips the short system message that asks the model to copy names and numbers exactly.

Every answer has the header X-PII-Replacements with the number of replaced values.

More examples

JavaScript (Node.js), streaming
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "http://pii.kwizmo.eu/gateway.php/v1", apiKey: process.env.KWIZMO_KEY });

const stream = await client.chat.completions.create({
  model: "gpt-4o-mini",
  stream: true,
  messages: [{ role: "user", content: "Povzemi pritožbo gospe Maje Kovačič, tel. 041 123 456." }],
  pii: { locale: "sl", known_names: ["Maja Kovačič"] },   // optional, removed before forwarding
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
curl
curl -s http://pii.kwizmo.eu/gateway.php/v1/chat/completions \
  -H "Authorization: Bearer pii_…" \
  -H "Content-Type: application/json" \
  -H "X-PII-Locale: hr" \
  -H "X-PII-Return-Mapping: true" \
  -d '{"model": "gpt-4o-mini",
       "messages": [{"role": "user", "content": "Pošalji podsjetnik Ivani Marić, OIB 69435151530."}]}'
PHP
<?php
// PHP 7.4+, no libraries needed
$ch = curl_init('http://pii.kwizmo.eu/gateway.php/v1/chat/completions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . getenv('KWIZMO_KEY')],
    CURLOPT_POSTFIELDS => json_encode([
        'model' => 'gpt-4o-mini',
        'messages' => [['role' => 'user', 'content' => 'Sehr geehrte Frau Müller, …']],
    ]),
]);
$reply = json_decode(curl_exec($ch), true);
echo $reply['choices'][0]['message']['content'];

Providers

The gateway works with any service that offers the OpenAI chat API, for example OpenAI, Azure OpenAI, Mistral (EU), Anthropic (OpenAI-compatible endpoint), Google Gemini (OpenAI-compatible endpoint), Groq, OpenRouter, and local models with Ollama, vLLM or LM Studio. On a self-hosted installation the operator chooses providers per model in app/conf.php:

app/conf.php
'gateway_enabled' => true,
'gateway_upstreams' => [
    // first match wins; "models" are prefixes, "*" matches everything
    ['name' => 'mistral', 'base_url' => 'https://api.mistral.ai/v1', 'api_key' => 'MISTRAL_KEY', 'models' => ['mistral-', 'codestral']],
    ['name' => 'local',   'base_url' => 'http://127.0.0.1:11434/v1', 'no_key' => true, 'models' => ['llama', 'qwen']],
    ['name' => 'openai',  'base_url' => 'https://api.openai.com/v1', 'api_key' => 'OPENAI_KEY', 'models' => ['*']],
],

What is replaced and restored

  • The same person gets the same fake name throughout the conversation, also in other grammatical cases (“Novak”, “Novaka”, “Novakom”).
  • When the model uses a fake name in a form that was not in your text (“Mlakarju”), the real name is still restored in the matching form (“Novaku”).
  • Streamed answers are held back only for as long as a fake value could still be incomplete, usually a few words.
  • If the model changes a fake value (translates, abbreviates or misspells it), that spot stays fake. The system message reduces this; check important answers.
  • Detection is automatic and not perfect: see the measured accuracy.

Errors and limits

Errors use the OpenAI format, so SDKs raise their usual exceptions. Errors from the provider are passed on unchanged.

StatusCodeMeaning
401api_key_required, invalid_api_keyMissing or wrong Kwizmo key.
401provider_key_requiredNo provider key stored for this model; send your own.
403api_key_scopeThe key is not allowed to use the gateway.
400model_not_availableNo provider is configured for this model.
413payload_too_large, text_too_longRequest larger than your key allows.
429rate_limited, quota_exceededPer-minute limit or monthly quota of the key reached. See Retry-After, X-RateLimit-* and X-Quota-*.
502upstream_unreachableThe provider did not answer.

Privacy

Requests and answers are processed in memory only. The gateway writes nothing about their content; it logs the time, the IP address and the API key id, and counts requests per key for billing. The provider receives only the protected text and remains a separate recipient under its own terms. For contracts with business customers we sign a data processing agreement.