Request Rewriter
Use request-rewriter to transform selected text fields immediately before the
gateway forwards an OpenAI-compatible request upstream. It can add one system
instruction and run ordered text rules. It never blocks a request.
Outcome
The gateway runs this as a dedicated pre-upstream step when
request-rewriter is in the effective route chain.
| Result | Value |
|---|---|
| Phase reported in the policy result | input |
| Verdict | always allow |
| Changed request reason | request_rewriter.applied |
| Unchanged request reason | request_rewriter.no_match |
Result details contain:
{
"rules_evaluated": 2,
"rules_applied": 1,
"system_message_applied": true
}
rules_applied counts rule applications across text fields, not unique rule
names. One rule that changes two messages contributes 2. The overall request
decision can still retain ok as its final reason; inspect the nested
request-rewriter result for these codes.
Prerequisites
- The client must send JSON in a supported Chat Completions or Responses input shape.
request-rewritermust be present in the effective global or route chain.- Define the block under
policy.request-rewriter. - Treat rewrites as deterministic transformations, not as a substitute for prompt-injection, DLP, or PII controls.
Configuration
pack:
name: request-rewriter-example-1
version: "1.0.0"
enabled: true
policies:
chain:
- request-rewriter
policy:
request-rewriter:
system_message:
content: Follow company policy.
rules:
- name: redact-email
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
replacement: "[EMAIL]"
position: replace
- name: add-prefix
replacement: '[SAFE] '
position: prepend
condition: confidential
Supported fields
| Field | Required | Default | Current behavior |
|---|---|---|---|
system_message | no | none | Non-empty string or { content: string }. |
rules | no | [] | Ordered list of rewrite rules. |
rules[].name | yes | none | Required by schema. Current result details report counts, not individual names. |
rules[].replacement | yes | none | Text used for replacement, prepend, or append. |
rules[].pattern | no | "" | Regex used only for position: replace. |
rules[].position | no | replace | replace, prepend, or append. |
rules[].condition | no | none | Non-empty, case-insensitive substring guard evaluated against the current text. |
The runtime has defensive defaults for missing rule fields, but schema-valid
configs must include name and replacement.
What gets rewritten
Only string values in these locations are changed:
| Request shape | Rewritten location |
|---|---|
| Chat Completions | messages[*].content |
| Responses with string input | input |
| Responses with array input | String input[*] items and string input[*].content values |
Multimodal arrays, nested content parts, tool definitions, tool arguments, metadata, and any other fields are left unchanged.
Rule semantics
Rules run in array order. Each rule sees the text produced by earlier rules.
| Position | Matching and mutation |
|---|---|
replace | Compiles pattern with the runtime's regex engine and replaces every match. Replacement capture references such as $1 are supported. |
prepend | Ignores pattern and adds replacement to the start unless the current text already starts with that exact string. |
append | Ignores pattern and adds replacement to the end unless the current text already ends with that exact string. |
condition is a case-insensitive substring test, not a regex. An invalid
replace regex is silently skipped at runtime and contributes zero
applications, so always verify a positive and negative example before rollout.
Ordered transformation example
With these rules:
rules:
- name: preserve-ticket-number
pattern: 'ticket ([0-9]+)'
replacement: 'case $1'
- name: add-classification
replacement: '[INTERNAL] '
position: prepend
condition: case
Review ticket 4821 becomes [INTERNAL] Review case 4821.
System-message behavior
The system message is injected before rewrite rules run. Rules can therefore also change the injected text.
- For Chat Completions, the gateway appends the configured text to the first
string-valued system message using a blank line. If none exists, it inserts a
system message at
messages[0]. - For a Responses string input, the gateway converts
inputinto a two-item system/user array. - For a Responses array, it appends to an existing string-valued system item or
inserts a system item at
input[0]. - If existing system content already contains the exact configured text, injection is skipped.
- When a request has a usable
messagesarray, injection stops there; it does not also inject intoinput.
Before and after
Input:
{
"model": "replace-with-model-id",
"messages": [
{
"role": "user",
"content": "Email alice@example.com about confidential pricing."
}
]
}
Forwarded text fields:
{
"messages": [
{
"role": "system",
"content": "Follow company policy."
},
{
"role": "user",
"content": "[SAFE] Email [EMAIL] about confidential pricing."
}
]
}
The provider, model, and other request fields remain in the forwarded request; they are omitted above only to focus on the transformed fields.
Validate and verify
kt policy test currently does not invoke the gateway's dedicated request
rewrite handler. A passing pack test with only this policy is therefore not
proof that a rewrite occurred.
Use this rollout loop instead:
kt policy lint --file policy-config.yaml
kt gateway run \
--listen 127.0.0.1:41002 \
--agent rewrite-verification \
--policy-config policy-config.yaml
In another terminal, send a representative request:
export KEEPTRUSTS_CLIENT_TOKEN="replace-with-separate-client-token"
curl -fsS http://127.0.0.1:41002/v1/chat/completions \
-H "Authorization: Bearer ${KEEPTRUSTS_CLIENT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"model": "replace-with-model-id",
"stream": false,
"messages": [
{
"role": "user",
"content": "Email alice@example.com about confidential pricing."
}
]
}'
Then correlate the request ID with kt events tail --since 10m --json and
confirm the nested result has:
reason_code: request_rewriter.applied;rules_appliedgreater than zero; and- the expected
system_message_appliedvalue.
For high-assurance rollout, send the gateway to a controlled inspection upstream and verify the exact forwarded JSON. Provider output alone can be ambiguous because a model may not reveal every request transformation.
Limitations
- The policy always allows; it cannot reject an unsafe transformed request.
- Invalid regexes are skipped without a policy error.
- String containment prevents duplicate prefix/suffix or system insertion only for an exact existing string.
- Non-string and unsupported content shapes are untouched.
- Rule names are not included in current policy-result details.
- This handler does not provide semantic rewriting or model-based classification.
- A request blocked by the pre-request chain returns before the dedicated rewrite step, so no rewritten payload is forwarded.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
request_rewriter.no_match | No supported string field changed, the condition missed, the regex missed, or the regex was invalid. | Inspect the request shape and test the exact pattern/condition against representative text. |
| System message was not added | It was empty, already contained in the existing system text, or neither messages nor input had a supported shape. | Use a non-empty string and inspect the JSON structure. |
| Prefix appears more than once across messages | Rules apply independently to every supported text field. | Add a narrower condition or reduce the fields that contain the trigger. |
| A multimodal content item is unchanged | Only string content fields are supported. | Put the governed text in a supported string field or use another policy designed for that payload. |
| Lint rejects a rule that runtime could default | name and replacement are schema-required. | Add both fields; do not rely on runtime defensive defaults. |
| Pack tests pass but live request is unchanged | kt policy test does not execute this runtime handler. | Run an integration request and inspect the nested decision result plus forwarded JSON. |
Next steps
- Response Rewriter — transform supported non-streaming output fields
- Prompt Injection — block adversarial instructions instead of merely rewriting text
- DLP Filter — enforce broader sensitive-data controls
- kt gateway run — run the integration verification path
- Investigate a Blocked Request — correlate policy results by request ID