Skip to main content

Request Rewriter

Use request-rewriter to change selected text fields before the gateway forwards an OpenAI-compatible request. It can add one system instruction. It can also run text rules in sequence. It does not block a request.

Outcome

The gateway runs a pre-upstream step when the effective route chain contains request-rewriter.

ResultValue
Phase reported in the policy resultinput
Verdictalways allow
Changed request causerequest_rewriter.applied
Unchanged request causerequest_rewriter.no_match

Result details contain:

{
"rules_evaluated": 2,
"rules_applied": 1,
"system_message_applied": true
}

rules_applied counts applications across text fields. It does not count unique rule names. One rule that changes two messages contributes 2. The last request cause can stay ok. Examine the nested request-rewriter result for rewrite codes.

Prerequisites

  • Send JSON in a supported Chat Completions or Responses input shape.
  • request-rewriter must be in the effective global or route chain.
  • Define the block in policy.request-rewriter.
  • Use rewrites as deterministic transformations. They do not replace 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

FieldNecessaryDefaultActive behavior
system_messagenononeNon-empty string or { content: string }.
rulesno[]List of rewrite rules in sequence.
rules[].nameyesnoneThe schema makes this field necessary. 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 condition evaluated against the input text.

A correct config must include name and replacement for each rule.

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

The policy does not change multimodal arrays or nested content parts. It also keeps tool definitions, tool arguments, metadata, and other fields.

Rule semantics

Rules run in array sequence. Each rule sees the text changed by earlier rules.

PositionMatching and mutation
replaceCompiles pattern with the runtime regex engine. It replaces each match and supports capture references such as $1.
prependIgnores pattern and adds replacement to the start unless the input text starts with that specified string.
appendIgnores pattern and adds replacement to the end unless the input text ends with that specified string.

condition is a substring test without case sensitivity. It is not a regex. The runtime skips an invalid replace regex. It records zero applications. Always test one matching and one nonmatching example before rollout.

Transformation sequence 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 gateway adds the system message before rewrite rules run. Thus, rules can also change the new text.

  • For Chat Completions, the gateway adds configured text to the first string system message. It uses two newline characters. If no system message is available, it adds one at messages[0].
  • For a Responses string input, the gateway changes input into a two-item system/user array.
  • For a Responses array, it adds text to a string system item. If no string system item is available, it adds a system item at input[0].
  • If system content contains the specified configured text, the policy does not add the text again.
  • When a request has a usable messages array, the policy adds the message there. It does not also add text to 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 forwarded request keeps the provider, model, and other fields. The example does not include them so that it shows only changed fields.

Validate and verify

verdictan policy test does not run the gateway request-rewrite handler. Thus, a passing pack test does not prove that a rewrite occurred.

Use this rollout loop:

verdictan policy lint --file policy-config.yaml
verdictan gateway run \
--listen 127.0.0.1:41002 \
--agent rewrite-verification \
--policy-config policy-config.yaml

In a different terminal, send a typical request:

export VERDICTAN_CLIENT_TOKEN="replace-with-separate-client-token"

curl -fsS http://127.0.0.1:41002/v1/chat/completions \
-H "Authorization: Bearer ${VERDICTAN_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 verdictan events tail --since 10m --json. Make sure that the nested result has:

  • reason_code: request_rewriter.applied.
  • rules_applied greater than zero.
  • the specified system_message_applied value.

For a high-assurance rollout, use a controlled inspection upstream. Verify the specified forwarded JSON. Provider output can be ambiguous because models do not show each request change.

Limitations

  • The policy always allows. It cannot reject a dangerous transformed request.
  • Invalid regexes are skipped without a policy error.
  • String containment prevents duplicate insertion only when the string is an exact match.
  • Non-string and unsupported content shapes are untouched.
  • Rule names are not included in active policy-result details.
  • This handler does not give semantic rewriting or model-based classification.
  • A pre-request block returns before the rewrite step. The gateway does not forward a changed payload.

Troubleshooting

SymptomLikely causeResolution
request_rewriter.no_matchNo supported string field changed, the condition missed, the regex missed, or the regex was invalid.Examine the request shape and test the specified pattern/condition against representative text.
System message was not addedIt was empty, was in the system text, or messages and input did not have a supported shape.Use a non-empty string and examine the JSON structure.
Prefix occurs multiple times across messagesRules apply independently to each supported text field.Add a narrower condition or decrease 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 a different policy for that payload.
Lint rejects a rulename or replacement is missing.Add the two necessary fields.
Pack tests pass but live request is unchangedverdictan policy test does not run this runtime handler.Send an integration request. Examine the nested result and forwarded JSON.

Next steps