Skip to main content

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.

ResultValue
Phase reported in the policy resultinput
Verdictalways allow
Changed request reasonrequest_rewriter.applied
Unchanged request reasonrequest_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-rewriter must 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

FieldRequiredDefaultCurrent behavior
system_messagenononeNon-empty string or { content: string }.
rulesno[]Ordered list of rewrite rules.
rules[].nameyesnoneRequired by schema. Current result details report counts, not individual names.
rules[].replacementyesnoneText used for replacement, prepend, or append.
rules[].patternno""Regex used only for position: replace.
rules[].positionnoreplacereplace, prepend, or append.
rules[].conditionnononeNon-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 shapeRewritten location
Chat Completionsmessages[*].content
Responses with string inputinput
Responses with array inputString 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.

PositionMatching and mutation
replaceCompiles pattern with the runtime's regex engine and replaces every match. Replacement capture references such as $1 are supported.
prependIgnores pattern and adds replacement to the start unless the current text already starts with that exact string.
appendIgnores 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 input into 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 messages array, injection stops there; it does not also inject into input.

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_applied greater than zero; and
  • the expected system_message_applied value.

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

SymptomLikely causeResolution
request_rewriter.no_matchNo 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 addedIt 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 messagesRules apply independently to every supported text field.Add a narrower condition or reduce the fields that contain the trigger.
A multimodal content item is unchangedOnly 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 defaultname and replacement are schema-required.Add both fields; do not rely on runtime defensive defaults.
Pack tests pass but live request is unchangedkt policy test does not execute this runtime handler.Run an integration request and inspect the nested decision result plus forwarded JSON.

Next steps