> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/howto/script-response-redact · Doc index: https://docs.gatellm.io/en/llms.txt


# Redact responses with a script

The response script (`response_transform`) post-processes the upstream's non-streaming response body: redact PII, inject request metadata, strip content to reduce cost. This chapter gives several practical examples.

Prerequisite: read [Write your first script transform](/en/howto/write-script-transform.md) first to understand the basic structure, mounting, and Context API.

> Streaming response scripts are not yet supported. `response_transform` only takes effect for non-streaming requests.

> **The body the response script sees is in the upstream protocol format.** The response pipeline order is: upstream response → **response script** → reverse protocol translation. That is, the script receives the **raw response body returned by the upstream** (an OpenAI upstream is `choices`, an Anthropic upstream is a top-level `content`, a Google upstream is `candidates`), and the gateway translates it back to the client protocol only after the script finishes. Therefore the examples below are all written for an **OpenAI** upstream (`choices`) — when the upstream is Anthropic / Google the body shape is different and must be branched by protocol, otherwise the script throws.

## Example 1: redact PII from the response

Replace email addresses in the response with `[REDACTED]` (**when the upstream is OpenAI**, iterate `choices`):

```javascript
function transform(body, context) {
    // The response body is in the upstream protocol format; this example is
    // based on an OpenAI upstream (body.choices)
    if (!Array.isArray(body.choices)) return body;
    for (let i = 0; i < body.choices.length; i++) {
        if (body.choices[i].message.content) {
            body.choices[i].message.content = regex_replace(
                body.choices[i].message.content,
                "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
                "[REDACTED]"
            );
        }
    }
    return body;
}
```

Mount on the `response_transform` field of a model or upstream. Before the response is returned to the client, all email addresses are replaced.

When the upstream is Anthropic the response body shape is different (top-level `content`, no `choices`); the same redaction must iterate `body.content`:

```javascript
function transform(body, context) {
    // Anthropic upstream: content is a block array
    if (!Array.isArray(body.content)) return body;
    for (const block of body.content) {
        if (block.type === "text" && block.text) {
            block.text = regex_replace(
                block.text,
                "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
                "[REDACTED]"
            );
        }
    }
    return body;
}
```

> Security advice: for redaction scripts, configure `script_error_mode = log-and-reject` to ensure that un-redacted content is not let through when the script fails.

## Example 2: add request metadata to the response

Inject the gateway's request ID and elapsed time into the response's `usage` field:

```javascript
function transform(body, context) {
    if (body.usage) {
        body.usage.gateway_request_id = context.requestId;
        body.usage.gateway_elapsed_ms = context.elapsedMs;
    }
    return body;
}
```

Mount on `response_transform`. The response's `usage` field gains `gateway_request_id` and `gateway_elapsed_ms`, making it easier for clients to troubleshoot.

## Example 3: strip image content (reduce cost)

Filter out image blocks from the response and keep the text. Note that it iterates `choices[].message.content` (**when the upstream is OpenAI**), not the top-level `messages` — the response body has no top-level `messages` field, and iterating it would **silently throw under the default `log-and-continue` and the filtering would have no effect at all**:

```javascript
function transform(body, context) {
    // The response body is in the upstream protocol format; this example is
    // based on an OpenAI upstream (body.choices)
    if (!Array.isArray(body.choices)) return body;
    for (const choice of body.choices) {
        const content = choice.message?.content;
        if (Array.isArray(content)) {
            choice.message.content = content.filter(block => block.type !== "image_url");
        }
    }
    return body;
}
```

> When the upstream is Anthropic, iterate `body.content` (a block array) and filter `block.type === "image"`, written the same way as Example 1's branch version.
>
> For simple scenarios prefer the `filter-content-types` request_payload rule over a script, for better performance.

## Example 4: inject a warning marker into the response

When the upstream response status code is not 200, inject a warning field into the response:

```javascript
function transform(body, context) {
    if (context.responseStatus >= 400) {
        body._gateway_warning = {
            reason: "upstream_error",
            status: context.responseStatus,
            elapsed_ms: context.elapsedMs
        };
    }
    return body;
}
```

## Context fields available in the response phase

| Field | Type | Description |
|------|------|------|
| `context.responseStatus` | number | Upstream HTTP status code |
| `context.responseHeaders` | object | Upstream response headers |
| `context.elapsedMs` | number | Elapsed time (milliseconds) |

Plus the fields also available in the request phase (`clientModel`, `upstreamModel`, `sourceProtocol`, `accessKeyName`, `accessKeyGroup`, `requestId`, etc.). See [Scripting API reference](/en/reference/scripting-api.md) for the full table.

> **Security**: sensitive headers such as `authorization`, `cookie`, and `x-api-key` are removed before scripts can see them, and `context.responseHeaders` also filters sensitive headers.

## Built-in function reference

Built-in functions commonly used for response redaction:

```javascript
// Regex replace
regex_replace("foo123bar", "[0-9]+", "N");  // "fooNbar"

// String operations
str_contains("hello world", "world"); // true
str_replace("foo-bar", "-", "_");    // "foo_bar"
str_starts_with("hello", "he");     // true
str_trim("  hello  ");              // "hello"

// Encoding
base64_encode("hello");    // "aGVsbG8="
base64_decode("aGVsbG8="); // "hello"
sha256("hello");           // "2cf24dba5fb0..."

// JSON operations
json_encode(body.choices);  // Serialize to string (OpenAI upstream response body)
json_parse(jsonStr);        // Parse from string
```

See [Scripting API reference](/en/reference/scripting-api.md) for the full built-in function table.

## Error handling

### script_error_mode

| Mode | Behavior | Use case |
|------|------|---------|
| `log-and-continue` (default) | On script error, log a warn entry and continue the request with the original body | Script failure should not block the request |
| `log-and-reject` | On script error, reject the request and return a gateway error | Script correctness is critical (e.g. security redaction) |

> For redaction scripts, configure `log-and-reject` to avoid un-redacted content flowing to the client when the script fails.

### You must return body on early return

```javascript
function transform(body, context) {
    // ✅ Correct
    if (!body.choices) return body;

    // ❌ Wrong: return with no value → the script silently fails
    if (!body.choices) return;

    body.choices[0].message.content = "modified";
    return body;
}
```

## FAQ

**Q: Does the response script work for streaming requests?**
Streaming response scripts are not yet supported. Only non-streaming requests. Request scripts work for streaming requests.

**Q: The script changed body.choices[0].message.content but the client didn't see the changed content?**
Check: ① whether the request is streaming (streaming does not run the response script); ② whether `script_error_mode` is `log-and-continue` (silently continues when the script fails); ③ whether the script is mounted on the correct model/upstream.

**Q: No Authorization in context.responseHeaders?**
Security design: sensitive headers such as `authorization`, `cookie`, and `x-api-key` are removed before scripts can see them, to prevent credential leakage.

**Q: How do I stay safe when a redaction script fails?**
Configure `script_error_mode = log-and-reject`. On failure the request is rejected, ensuring un-redacted content does not flow to the client.

**Q: The response body is large and the script runs slowly?**
The main cost of script execution comes from JSON serialization/deserialization. The bigger the body, the slower. For simple field operations prefer `request_payload` (no serialization cost). Avoid unnecessary `deep_clone` / `json_encode` + `json_parse`.

**Next**: [Scripting API reference](/en/reference/scripting-api.md) for the full built-in function tables; [Scripting config](/en/reference/scripting-config.md) for runtime limits and configuration; [Audit & security config](/en/reference/audit-and-security-config.md) for other security settings.
