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.
| Result | Value |
|---|---|
| Phase reported in the policy result | input |
| Verdict | always allow |
| Changed request cause | request_rewriter.applied |
| Unchanged request cause | request_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-rewritermust 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
| Field | Necessary | Default | Active behavior |
|---|---|---|---|
system_message | no | none | Non-empty string or { content: string }. |
rules | no | [] | List of rewrite rules in sequence. |
rules[].name | yes | none | The schema makes this field necessary. 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 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 shape | Rewritten location |
|---|---|
| Chat Completions | messages[*].content |
| Responses with string input | input |
| Responses with array input | String 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.
| Position | Matching and mutation |
|---|---|
replace | Compiles pattern with the runtime regex engine. It replaces each match and supports capture references such as $1. |
prepend | Ignores pattern and adds replacement to the start unless the input text starts with that specified string. |
append | Ignores 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
inputinto 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
messagesarray, the policy adds the message there. It does not also add text toinput.
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_appliedgreater than zero.- the specified
system_message_appliedvalue.
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
| Symptom | Likely cause | Resolution |
|---|---|---|
request_rewriter.no_match | No 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 added | It 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 messages | Rules apply independently to each supported text field. | Add a narrower condition or decrease 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 a different policy for that payload. |
| Lint rejects a rule | name or replacement is missing. | Add the two necessary fields. |
| Pack tests pass but live request is unchanged | verdictan policy test does not run this runtime handler. | Send an integration request. Examine the nested result and forwarded JSON. |
Next steps
- Response Rewriter — transform supported non-streaming output fields
- Prompt Injection — block adversarial instructions, not only rewrite text
- DLP Filter — enforce broader sensitive-data controls
- verdictan gateway run — run the integration verification path
- Investigate a Blocked Request — correlate policy results by request ID