Response Rewriter
Use response-rewriter to change text in supported OpenAI-compatible JSON responses. The responses must not use streaming. The policy can replace, prepend, or append text. It keeps the other JSON fields.
Outcome
| Result | Value |
|---|---|
| Phase | output |
| Verdict | always allow |
| A minimum of one rule changed text | rewriter.applied |
| No supported text changed | rewriter.no_match |
Policy-result details contain:
{
"rules_evaluated": 2,
"rules_applied": 2,
"structure_preserved": true
}
The gateway adds structure_preserved after it changes the JSON body. rules_applied counts applications across response segments. One rule on two segments contributes 2.
The policy does not block. A changed response keeps the last verdict allow. Its nested cause can be rewriter.applied.
Prerequisites
- Make sure that the upstream returns a non-streaming JSON body in a supported response shape.
- Add
response-rewriterto the effective policy chain and configurepolicy.response-rewriter. - Use a different block or redaction policy for dangerous content. Rewriting always allows the response.
Configuration
pack:
name: response-rewriter-example-1
version: "1.0.0"
enabled: true
policies:
chain:
- response-rewriter
policy:
response-rewriter:
rules:
- name: append-disclaimer
replacement: "\n\nAI output — verify independently."
position: append
- name: redact-email
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
replacement: "[EMAIL]"
position: replace
Supported fields
| Field | Necessary | Default | Active behavior |
|---|---|---|---|
rules | no | [] | List of rewrite rules in sequence. |
rules[].name | yes | none | The schema makes this field necessary. Active 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 on the input text segment. |
A correct config must include name and replacement for each rule.
Supported response fields
Only string values at these paths are changed:
| Response family | Rewritten path |
|---|---|
| Chat Completions | choices[*].message.content |
| Responses | output[*].content[*].text |
The policy does not change top-level output strings or tool-call arguments. It
also keeps audio, images, arbitrary JSON, plain text, and invalid JSON. The
gateway returns an unsupported body without changes.
A match-based replacement usually causes rewriter.no_match. An
unconditional prepend or append can report rewriter.applied without a
supported body change. Verify the caller body and structure_preserved. Do not
use only the cause code.
Rule semantics
Rules run in array sequence for each supported text segment.
| 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 unless the segment starts with that specified string. |
append | Ignores pattern and adds replacement unless the segment ends with that specified string. |
condition is a substring test without case sensitivity. It checks text after earlier rules. The runtime skips an invalid replace regex. It records zero applications.
Capture-group example
policy:
response-rewriter:
rules:
- name: normalize-ticket-reference
pattern: 'ticket ([0-9]+)'
replacement: 'case $1'
Review ticket 4821 becomes Review case 4821.
Structure preservation
For supported JSON responses, the runtime does these steps:
- Parse the outer body.
- Change only supported string fields.
- Serialize the same other JSON fields.
The policy does not give a whole-body text mode.
Streaming boundary
Do not use response-rewriter for stream: true SSE traffic. The streaming path does not run this evaluator. Use stream: false when this change is necessary. Test each transport path before rollout.
Validate and verify
verdictan policy test does not run the gateway response-rewrite handler. Use a running gateway and a controlled upstream response:
verdictan policy lint --file policy-config.yaml
verdictan gateway run \
--listen 127.0.0.1:41002 \
--agent response-rewrite-verification \
--policy-config policy-config.yaml
Send a non-streaming 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": "Reply with one short sentence."
}
]
}'
Verify all three layers:
- Verify that the caller receives the specified changed field and a correct provider response envelope.
- Verify that
verdictan events tail --since 10m --jsonshowsrewriter.appliedand the specifiedrules_applied. - Send a negative request when no unconditional prepend or append rule is available.
- Verify that this request causes
rewriter.no_match.
Use a deterministic test upstream for rollout tests. Model wording is not a stable test fixture for a regex contract.
Rollout guidance
- Start with one narrow replacement. Test typical matching and nonmatching samples.
- Examine each response family that the application uses. Support for one JSON path does not prove support for a different response family.
- Measure added response latency. The policy must examine the body before it changes the response.
- Keep a regression case for structure, not only the rewritten string.
- Put block policies before cosmetic rewrites. Then, a block stops the rewrite.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
rewriter.no_match | Pattern or condition missed, regex was invalid, or the response path is unsupported. | Examine the actual upstream JSON and test the specified text segment. |
| Lint rejects a rule | name or replacement is missing, or an enum/type is invalid. | Add the necessary schema fields and use replace, prepend, or append. |
| Event says allow but content changed | Rewriting is allow-only. | Examine the nested response-rewriter result. A block verdict does not occur. |
| Streamed output is unchanged | SSE output does not run this evaluator today. | Use stream: false for this control. |
| One segment changed more than specified | Rules run independently and in sequence on each supported segment. | Narrow the pattern or add a condition. Examine rules_applied. |
Next steps
- Request Rewriter — transform supported request fields before forwarding
- UPL Filter — add legal-advice-specific output controls
- Safety Filter — use a blocking output control where delivery must stop
- verdictan gateway run — do transport-level verification
- Policy Controls Catalog — compare output controls