# How to redact PII before sending prompts to OpenAI (TypeScript)

> In TypeScript, redact supported matches before the provider call and restore recognised reply tokens with the matching vault. `@raeven-co/sether` offers stream transforms, one-shot helpers and wrappers for supported client paths. Inspect the edited payload and test your response format; JSON replies need JSON-aware restoration rather than raw text replacement.

Published 2026-07-09, updated 2026-10-17 by Godfrey Lebo.

## TL;DR

- The API sends the payload you supply. HTTPS protects transport, but the intended provider can read the request content.
- Keep the matching vault available in both directions; reusing one instance is the simplest configuration.
- wrapOpenAI wraps supported client paths; test the SDK methods and response formats your app actually uses.
- Streaming helpers preserve context across chunks, with bounded safeguards and format-specific limits to test.
- Test replacement coverage, known-map restoration, missing mappings and the actual outbound request; one round-trip assertion is only the start.

## What actually leaves your server when you call OpenAI?

Open the network tab on any AI chat feature and look at the request body. The `messages` array contains exactly what the user typed: their name, their email, the card number they pasted while asking about a refund.

In a local demo, inspect a fictional prompt in the request body. A visible sample email there is application content the provider will receive; it does not mean HTTPS is disabled. Provider SDKs do not automatically remove personal details simply because those details look sensitive.

The fix is a redaction layer in the request path: replace personal values with tokens before the request leaves, and put the real values back in the reply. This post is the hands-on TypeScript version; the concepts are covered in the [complete guide to PII redaction for LLM apps](/blog/pii-redaction-for-llm-apps-complete-guide).

## Install

```bash
npm install @raeven-co/sether
```

The package is MIT-licensed and provides ESM, CJS and a browser entry at `@raeven-co/sether/browser`. Its one declared TypeScript runtime dependency is `libphonenumber-js` for phone validation. Check the selected release’s engine requirements and build footprint when adding it to your deployment.

## The 3-step round trip

The core pattern is three steps: redact the prompt, call OpenAI, restore the reply. One Sether instance handles both directions, because the instance holds the vault that maps tokens back to real values.

```ts
import { Sether } from '@raeven-co/sether';
import { Readable } from 'node:stream';
import { text } from 'node:stream/consumers';

const sether = new Sether();

// 1. Redact the user message before it leaves
const safePrompt = await text(
  Readable.from([userMessage]).pipe(sether.redact())
);

// 2. Call OpenAI with the redacted prompt
const completion = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: safePrompt }],
});

// 3. Restore real values in the reply, same instance
const reply = await text(
  Readable.from([completion.choices[0].message.content ?? ''])
    .pipe(sether.restore())
);
```

If you are not streaming and just want a string in and a string out, the exported `redactSync(text, { detectors, vault })` and `restoreSync(text, { vault })` functions skip the stream plumbing. The important rule is unchanged: both sides must use the same vault, usually `sether.vault`, because that is where tokens map back to values.

The retained vault is the requirement; the object identity is a convenient way to satisfy it. Separate instances can use the same configured vault. The deeper explanation is in [Reversible PII redaction: how redact-then-restore works](/blog/reversible-pii-redaction-redact-restore).

## Can I do this without changing my call sites?

Sether ships `wrapOpenAI` for supported client methods, redacting request text and restoring supported reply content. Keep a fixture for each method, SDK shape and response format you rely on; a wrapper is not evidence that every future SDK feature is covered.

One design note worth knowing: the wrappers are structurally typed. Sether never imports the OpenAI SDK as a dependency, so you do not inherit a version pin or an extra package. The same applies to `wrapAnthropic`, `wrapFetch`, and `createExpressMiddleware`.

## What gets detected out of the box?

| Pack | Detectors | Enabled by default |
| --- | --- | --- |
| Basic | Email, phone (libphonenumber-js), credit card (regex + Luhn), SSN, IPv4, IPv6, IBAN (mod-97) | Yes |
| Secrets | AWS access key, OpenAI key, Anthropic key, GitHub PAT, Slack token, Stripe key, JWT, high-entropy strings | Opt-in |
| Identity | Label-anchored name, DOB, passport, address, with multilingual labels across Latin, CJK, Cyrillic, and Arabic scripts | Opt-in |

A card candidate must pass Luhn and an IBAN candidate must pass mod-97 in the configured detectors. Those checks reduce some invalid matches; they do not prove the candidate belongs to a person or establish a measured false-positive rate for your inputs.

The opt-in secrets pack includes fixed-format and entropy-based detectors. Entropy is a signal, not proof of a secret, so test ordinary high-entropy content and the actual key formats you use. Enable additional packs deliberately and review both missed and unwanted replacements.

## How do I handle streaming responses?

Most chat UIs stream over Server-Sent Events, and streaming is where naive redaction breaks. An email split across two chunks as `sarah@ac` and `me.com` passes a per-chunk regex on both sides.

Sether keeps recent text to provide context across chunks, using `safeDistanceBytes` (default 256) and bounded buffering guards. For SSE, use the framing-aware helpers and test your event shape. A JSON document assembled inside streamed content fragments needs to be collected and restored as JSON; see the [0.8.0 release limits](/blog/sether-0-8-0-json-safe-restore).

The [chunk-boundary post](/blog/streaming-llm-redaction-chunk-boundary-bug) explains the failure mode with a controlled example, partition tests, encoding questions and documented buffer limits.

## What do the tokens look like?

Tokens include a type prefix and a fresh identifier per match, such as `<EMAIL_uuid>`. The prefix hints at the field’s role. Test whether the model preserves and places tokens correctly; it may still change or omit one, and surrounding text may remain identifying.

The default vault is `MemoryVault`, an in-process LRU holding 10,000 entries with a one-hour TTL. The interface is pluggable, but TypeScript vault lookups are synchronous: do not substitute an async Redis get() implementation and expect text restore to await it. Use a compatible mapping strategy and test missing entries.

## How do I test the integration?

A round-trip assertion checks that known replacements resolve with the corresponding vault. Add a separate assertion that the outgoing sample no longer contains the values you intended to replace. Otherwise an unchanged string could pass a round-trip test without detecting anything.

```ts
import { Sether } from '@raeven-co/sether';
import { Readable } from 'node:stream';
import { text } from 'node:stream/consumers';
import assert from 'node:assert';

const roundTrip = async (input: string) => {
  const sether = new Sether();
  const redacted = await text(
    Readable.from([input]).pipe(sether.redact())
  );
  assert.ok(!redacted.includes('sarah@acme.com')); // value is gone
  const restored = await text(
    Readable.from([redacted]).pipe(sether.restore())
  );
  assert.strictEqual(restored, input); // and comes back exactly
};

await roundTrip('Hi, I am Sarah, my email is sarah@acme.com');
```

Also test chunk partitions, repeated values, missing or expired mappings and a captured request body from your actual wrapper. For structured replies, validate the parsed keys and schema after JSON-aware restore. The [current release account](/blog/sether-0-8-0-json-safe-restore) explains why text substitution is not safe for serialized JSON.

The full TypeScript SDK, including decoys and restoration, is free under MIT with no Sether usage quota. Optional Pro applies to convenience workflows in official tools, not these library calls. If you need a realistic stand-in rather than a typed token, evaluate the alias APIs separately and validate the task with those stand-ins. [Pricing](/pricing) explains the product boundary.

## Limitations, honestly

- Free-text names are not caught by the core package. "Tell Chidi I will send the money Friday" has no label and no structure, so it needs the NER add-on. The trade-offs are in [Regex vs NER for PII detection](/blog/regex-vs-ner-pii-detection).
- The identity pack is label-anchored. It catches "name: Sarah Chen" in many scripts, but not names floating in prose.
- The default vault is in-memory. If your redact and restore steps run in different processes, plug in a shared vault or the tokens cannot be resolved.
- Redaction is a technical measure that helps with data minimisation. It does not, on its own, make you compliant with anything. See [the GDPR post](/blog/is-it-gdpr-compliant-to-send-customer-data-to-openai) for the legal side.

You can try all of this without installing anything in the [live sandbox](/sandbox), and the full API is documented at [/docs](/docs). The source is at [github.com/raeven-co/sether](https://github.com/raeven-co/sether).

## Frequently asked questions

### Does this work with the OpenAI streaming API?

Supported stream paths can restore recognised tokens, and framing-aware helpers are available for SSE. Test the exact transport, final fragment and missing-map behavior. Streamed JSON-mode content has additional limits described in the [0.8.0 release](/blog/sether-0-8-0-json-safe-restore).

### Do I need to change my OpenAI SDK version?

The wrapper uses structural types rather than importing the provider SDK as a runtime dependency. That removes a package dependency, not the need for compatibility tests: changed SDK methods or response shapes can still require integration work.

### Will OpenAI answer worse because it sees tokens instead of real values?

It can change output quality or correctness. Evaluate your task with redacted fixtures, check for missing or altered tokens and validate restored output. Do deterministic checks such as email-format validation in your own code when possible.

### Is this enough for GDPR?

Redaction is a technical measure that helps, particularly with Article 5 data minimisation and Article 32 technical measures. It does not replace processor agreements or a lawful basis. The legal reasoning is in our GDPR post.


Canonical: https://sether.ai/blog/redact-pii-before-openai-typescript
