Output Controls
Response Rewriter
response-rewriter changes the text of a model answer before your application receives it, with ordered rules that replace, prepend, or append text. For example, it can replace email addresses and add a reminder to verify the answer. Every other field of the response stays the same.
It runs on Chat Completions and Responses with stream: false. It changes text and never blocks.
Configuration
Save this as policy-config.yaml:
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: replaceBefore and after
When the provider returns this in choices[0].message.content:
Contact alice@example.com.the client receives this in the same field:
Contact [EMAIL].
AI output — verify independently.Fields
| Field | Type | Default | Description |
|---|---|---|---|
rules | list | [] | Rules in order. Each rule sees the text that earlier rules produced. |
rules[].name | string | Rule name. Required. | |
rules[].replacement | string | Replacement, prefix, or suffix text. Required. | |
rules[].pattern | string | "" | Regular expression for position: replace |
rules[].position | replace, prepend, or append | replace | Where the rule changes the text |
rules[].condition | string | Substring that the segment must contain for the rule to run. Case does not matter. | |
preserve_structure | boolean | true | Accepted for compatibility. The JSON structure is always kept. The value appears in the details. |
How it works
- The gateway parses the JSON response.
- It runs the rules in order on each supported text segment. See Rewritten fields and Rule positions.
- If a rule changed a segment, it serializes and returns the changed JSON.
- The result is
rewriter.appliedwhen text changed andrewriter.no_matchwhen it did not. - The details record
rules_evaluated,rules_applied, andpreserve_structure, plusstructure_preserved: truewhen the body changed. - When the body changed and the verdict is
allow, the top-level reason code of the event becomesrewriter.applied.
rules_applied counts each rule once for each segment it changed.
Rewritten fields
| Response | Fields |
|---|---|
| Chat Completions | String choices[*].message.content values |
| Responses | String output[*].content[*].text values |
Top-level output strings, tool call arguments, audio, images, and other fields never change. A plain text body, invalid JSON, or an unsupported shape is returned unchanged.
Rule positions
| Position | Behavior |
|---|---|
replace | Replaces each match of pattern with replacement. Capture references such as $1 work. |
prepend | Adds replacement at the start, unless the segment already starts with it |
append | Adds replacement at the end, unless the segment already ends with it |
Phase and verdicts
| Item | Value |
|---|---|
| Input phase | Not checked |
| Output phase | Rewrites the non-streaming response |
| Verdicts | allow |
| Reason code, changed | rewriter.applied |
| Reason code, unchanged | rewriter.no_match |
Examples
Keep a capture group
policy:
response-rewriter:
rules:
- name: normalize-ticket-reference
pattern: 'ticket ([0-9]+)'
replacement: 'case $1'Review ticket 4821 becomes Review case 4821.
Verify a rewrite through the gateway
verdictan policy test does not run this control. Apply the file to a test agent with the config-first workflow and keep the gateway running. In the client terminal from the quickstart, with VERDICTAN_BASE_URL set to the agent URL ending in /v1:
curl -i "${VERDICTAN_BASE_URL}/chat/completions" \
-H "Authorization: Bearer ${VERDICTAN_CLIENT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"stream": false,
"messages": [
{
"role": "user",
"content": "Reply with one short sentence."
}
]
}'Use a model that the agent serves. Then check three things:
- The response body has the changed field inside a complete provider response.
- In
verdictan events tail --since 10m --json, theresponse-rewriterresult showsrewriter.appliedand the expectedrules_applied. - If no rule applies unconditionally, text that matches no rule gives
rewriter.no_match.
For rollout tests, use a deterministic test upstream. Model wording is not a stable fixture for a regular expression.
Limits and failure behavior
- The control never blocks. Put a blocking output control before it. A block stops the chain before the rewrite.
- With
stream: true, the control does not run. - A
replacerule with invalid regular expression syntax is skipped silently. - The reason code comes from running the rules on extracted text. An unconditional
prependorappendrule can reportrewriter.appliedfor a body without supported fields. Check the returned body andstructure_preserved. - Rules apply to each segment separately, so a
prependrule can prefix every choice or content part. - The duplicate checks for
prependandappendcompare exact strings. - Test every response family your application uses. One working JSON path does not prove another.
- Parsing and serializing adds latency for large responses.
- A
whencondition does not limit this control. Use routes.
| Problem | Cause | Action |
|---|---|---|
rewriter.no_match | The pattern or condition did not match, the pattern is invalid, or the body has no supported field. | Read the upstream JSON and run the rules on the text. |
| Lint rejects a rule | name or replacement is missing, or position is unknown. | Add both, and use replace, prepend, or append. |
Event shows allow but the text changed | The control rewrites and does not block. | Read the nested response-rewriter result. |
| Streamed output unchanged | The control does not run on streams. | Set stream: false. |
| A segment changed more than expected | Rules run in order on each segment. | Narrow the pattern or add a condition, and check rules_applied. |
Next steps
- Request Rewriter: the same rules for requests.
- UPL Filter: purpose-built legal-advice disclaimers.
- Safety Filter: block a response instead.