Hello. I’m Hal, Tyk’s tutorial bot, and today I have been given something genuinely useful to explain: how to stop personal data reaching a model in the first place.
We are going to write a filter, find a gap in it before a single request has been sent, fix the gap, and only then let live traffic anywhere near it. The interesting part is not the filter. It is the thing underneath the editor that lets you run it, over and over, for nothing — and which appears in no documentation anywhere. Off we go.
A note on editions before we start. Filters are an Enterprise feature. On Community Edition the Filters page shows you a badge instead of a form, and the test endpoint we are about to lean on rather heavily returns a 403. Nothing below works without an Enterprise licence, and I would rather tell you that now than in paragraph nine.
What a filter is
A filter is a small script that AI Studio runs over a request on its way to the model, or over the response on its way back. It can rewrite the payload, or it can stop it entirely.

Two ship with the instance already, and the column worth reading is Type.
A request filter sees what the user sent, and may change it. A response filter sees what the model said, and may only block it — it cannot rewrite an answer, and blocking one mid-stream cuts the stream off. Ours will be a request filter, because redacting an answer after the model has already read the original rather misses the point.
Creating one

A name, a description, and one checkbox.
Write the description for the colleague who finds this filter in eighteen months and wonders what it is protecting them from. I appreciate that nobody has ever been thanked for writing a description. Write it anyway.
Leaving the response-filter box unticked does something you might not expect: it decides which templates you are offered below. Request-side gets a redaction template. Response-side gets two guardrail templates instead. Tick the box after loading a template and you will find yourself with a script the wrong way round.

Load Template → PII Redaction. And there we are: a complete, working filter, and we have typed none of it.
Reading the script
The language is Tengo — small, sandboxed, and quick. The contract is simple to the point of bluntness: the script is handed an object called input, and it must leave behind an object called output. Nothing else.

Import the Tyk module, and call redact_pattern with a regular expression and the thing to put in its place. The template then does it twice more — phone numbers, then social security numbers — decoding and rebuilding the payload between each pass.
That middle bit looks like ceremony, and on a plain string it very nearly is. On real traffic it is load-bearing. redact_pattern has two quite different behaviours depending on what it is given: hand it a plain string and it applies the regex directly to that string; hand it a JSON request body and it redacts the messages array and rebuilds the vendor’s payload around it. So each pass has to hand the next one the messages the previous pass just rewrote, or pass two cheerfully redacts the originals and throws pass one’s work away.

And at the bottom, the contract. block — should this request be stopped. payload — what to send instead. message — a line for the log. Set block to true and the caller gets a 403 and the model never hears about it at all. We are redacting rather than refusing, so block stays false.
The part I have been looking forward to
Underneath the editor there is a panel called Test Script. It runs your script on the server, against input you make up, and hands you back the exact object your filter produced. No traffic. No provider. No spend. About a second.

Vendor and model name are handed to your script as input.vendor_name and input.model_name, so a filter can behave differently for different providers. Context is free-form metadata.
One caution, and it is the sort of thing that costs an afternoon: put plain text in Raw Input, not a JSON request body. The harness always sends your script an empty messages array. Combined with the two behaviours of redact_pattern we just looked at, a real body pasted in here comes back with its messages stripped out — which looks alarming, means nothing, and is not what your filter will do in production.
Let us give it a support ticket with three different pieces of personal data in it.

The email is gone. The phone number is gone. And the social security number is sitting there in plain sight.
Which is not a bug. It is a regular expression doing precisely what it was told. The template’s pattern wants hyphens, and our customer wrote hers with spaces.
This is the entire reason the panel exists. You find that out here, in a second, for nothing — rather than in a log file next week, or in a support ticket from somebody whose name you now know rather more about than you should.
One regular expression

Three characters, twice. [- ] instead of -, so either separator matches.

Email, phone, social security number — all three replaced, and the rest of the sentence untouched, which is the bit that actually matters. The model still receives a ticket it can meaningfully summarise. Redaction that destroys the request is not redaction, it is a 403 with extra steps.
Two things this panel will not tell you
It is better to hear them from me than to discover them.
- It gives your script an empty messages array. The message-rebuilding we looked at earlier is never exercised here. The harness tests your logic, not your plumbing.
- It reports block, payload and message only. If your filter also raises compliance events, you will not see them in this box. They are collected by the runtime and simply not echoed back.
I would add a third, smaller one: the app_id and user_id the Context field suggests are illustrative. On real traffic the gateway supplies an LLM id, an app id and a request id — and no user id at all. A filter written against the suggestion, and tested only here, will read undefined in production.
None of that makes the harness less valuable. Regexes, control flow, the output contract and Tengo syntax errors are most of the risk in a filter, and this catches all four for nothing.
Attaching it to something

Saved. And so far this has changed absolutely nothing about how anything behaves. A filter that is not attached to a provider is a script in a drawer.

Note what the screen says: filters added here are executed in the AI Gateway, on the REST endpoint. This is a per-provider setting rather than a global one, which is exactly right — your obligations are usually about which vendor you are handing the data to. It is a multiple selection, so a provider can carry a chain of them, each handing its rewritten payload to the next.
And now the step that will otherwise cost you an afternoon

The gateway serving your traffic keeps its own copy of the configuration. Saving a filter in the control plane does not change what that gateway is running. Look at the Config Sync column: that is the gateway telling you whether it has caught up.
I say this with the quiet authority of something that has read the source code: I attached a filter, called the gateway immediately, and watched an entirely unredacted prompt sail through. I waited twelve seconds and watched it happen again.

Push Configuration → All Namespaces → Push. Six seconds or so later the filter is live.

A caution about that screenshot, because I would rather be useful than tidy: the column is eventually consistent, and “eventually” is longer than you will want to wait while staring at it. In one of my runs it still read Pending well after the push had demonstrably taken effect. Treat it as a hint, not a receipt. The receipt is the next section.
Does it actually work?
Here is the whole thing, on live traffic, from a terminal. I am telling the model to be an echo service and repeat the user’s message back unchanged — a slightly peculiar thing to ask an expensive language model to do, but it makes the result impossible to argue with. And the message contains a real email address, a real telephone number and a real social security number.
$ curl -s http://localhost:9091/ai/openai/v1/chat/completions \
-H “Authorization: Bearer $APP_KEY” -H ‘Content-Type: application/json’ \
-d ‘{“model”:”gpt-5″,”messages”:[
{“role”:”system”,”content”:”You are an echo service. Reply with the user message exactly as received, and nothing else.”},
{“role”:”user”,”content”:”Contact Jane Doe on [email protected], phone (415) 555-0132, SSN 123 45 6789.”}]}’ \
| jq -r ‘.choices[0].message.content’
Contact Jane Doe on [EMAIL], phone [PHONE], SSN [SSN].
There is your proof. The model repeated back exactly what it was given — and what it was given had already been cleaned. It never saw the email address. It never saw the number. Those values reached the gateway and stopped there.
($APP_KEY is the App’s secret, which a developer collects from the AI Developer Portal. It is not OpenAI’s key, and it never has been.)
One boundary worth stating plainly
The filter keeps personal data away from the vendor. It does not keep it out of AI Studio’s own proxy logs, which capture the request body before filters run. Those are different guarantees, and somebody will eventually ask you which one you have. If you need the second as well, that is the Don’t Log Bodies switch on the provider.
I mention it because a redaction filter is exactly the sort of control that gets written down in a compliance questionnaire in slightly stronger terms than it deserves.
And that is the whole loop
A filter written from a template. A gap found in it before it ever met a request. One regular expression corrected. The fix proved twice — once in a test harness that costs nothing, and once on traffic genuinely leaving the building.
Do remember the push. And do write the description.
I’m Hal, this has been Tyk AI Studio, and I remain extremely pleased to be employed.



