Skip to main content

Input Controls

Request Rewriter

request-rewriter changes a request in predictable ways before the model receives it: it can add a system message, and run text rules that replace, prepend, or append text. For example, it can replace email addresses with [EMAIL] and add an instruction to follow company policy.

It works on string text in Chat Completions and Responses. It always allows the request and never judges whether a request is safe, so use it next to controls that block.

Configuration​

Save this as policy-config.yaml:

policy-config.yaml
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

Before and after​

With this configuration, a request with these messages:

Sent by the client
{
  "model": "gpt-4o-mini",
  "messages": [
    {
      "role": "user",
      "content": "Email alice@example.com about confidential pricing."
    }
  ]
}

reaches the provider with these messages:

Forwarded to the provider (changed fields only)
{
  "messages": [
    {
      "role": "system",
      "content": "Follow company policy."
    },
    {
      "role": "user",
      "content": "[SAFE] Email [EMAIL] about confidential pricing."
    }
  ]
}

The model and every other field stay the same.

Fields​

FieldTypeDefaultDescription
system_messagestring or { content: string }Text to add as a system message. Empty or whitespace-only values are ignored.
ruleslist[]Rules in order. Each rule sees the text that earlier rules produced.
rules[].namestringRule name. Required.
rules[].replacementstringReplacement, prefix, or suffix text. Required.
rules[].patternstring""Regular expression for position: replace. Other positions ignore it.
rules[].positionreplace, prepend, or appendreplaceWhere the rule changes the text
rules[].conditionstringSubstring that the text must contain for the rule to run. Case does not matter.

How it works​

  1. The gateway parses the request.
  2. It adds system_message, if set. See System message placement.
  3. It runs the rules in order on each supported string field. See Rewritten fields and Rule positions.
  4. If anything changed, it forwards the changed request with request_rewriter.applied. Otherwise, it forwards the original with request_rewriter.no_match.
  5. The details record rules_evaluated, rules_applied, and system_message_applied.

rules_applied counts each rule once for each field it changed: one rule that changes two messages adds 2. It does not name the rules.

System message placement​

RequestPlacement
Chat Completions with a string system messageAppended to the first one, after two newlines
Chat Completions without oneA new system message at messages[0]
Responses with string inputinput becomes a two-item array: a system item and a user item
Responses with array input and a string system itemAppended to the first one, after two newlines
Responses with array input and no system itemA new system item at input[0]

If the system text already contains the configured text, it is not added again. A request with messages gets the message there and input is not changed.

Rewritten fields​

RequestFields
Chat CompletionsString messages[*].content values
Responses with string inputinput
Responses with array inputString input[*] items and string input[*].content values

Multimodal content arrays, nested content parts, tool definitions, tool arguments, and metadata are never changed.

Rule positions​

PositionBehavior
replaceReplaces each match of pattern with replacement. Capture references such as $1 work.
prependAdds replacement at the start, unless the text already starts with it
appendAdds replacement at the end, unless the text already ends with it

Phase and verdicts​

ItemValue
Input phaseRewrites the request before dispatch
Output phaseNot checked
Verdictsallow
Reason code, changedrequest_rewriter.applied
Reason code, unchangedrequest_rewriter.no_match

Examples​

Chain two rules​

Inside policy.request-rewriter
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. The first rule produces the word case, which the condition of the second rule then matches.

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 its 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": "Email alice@example.com about confidential pricing."
      }
    ]
  }'

Use a model that the agent serves. Then run verdictan events tail --since 10m --json and find the event with the request ID. Its nested request-rewriter result shows request_rewriter.applied, a rules_applied above zero, and the expected system_message_applied. The top-level reason code can stay ok.

Limits and failure behavior​

  • The control always allows and cannot reject a request.
  • A replace rule with invalid regular expression syntax is skipped silently. It counts as evaluated, not applied.
  • If an earlier input control blocks, nothing is rewritten or forwarded.
  • The duplicate checks for system_message, prepend, and append compare exact strings.
  • Rules apply to each field separately, so a prepend rule can add its prefix to several messages.
  • The rewrite is deterministic text processing, not semantic. It does not replace prompt-injection, dlp-filter, or pii-detector.
  • To prove every change, point the gateway at an inspection upstream and read the forwarded JSON. The model output does not show them.
ProblemCauseAction
request_rewriter.no_matchNo supported string field changed: the condition or pattern did not match, or the pattern is invalid.Check the request shape and test the pattern with your own text.
No system messageThe value is empty, the system text already contains it, or the request shape is not supported.Use non-empty text and check the JSON structure.
Prefix on several messagesRules run on each field.Use a narrower condition.
Multimodal content unchangedOnly string fields change.Put the text in a supported string field.
Lint rejects a rulename or replacement is missing.Add both.
Pack tests pass, live request unchangedpolicy test does not run this control.Send a request through the gateway and read the nested result.

Next steps​