Skip to content
This page is a translation of the authoritative Chinese source and may lag behind.View the original

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 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

FieldTypeDescription
context.responseStatusnumberUpstream HTTP status code
context.responseHeadersobjectUpstream response headers
context.elapsedMsnumberElapsed time (milliseconds)

Plus the fields also available in the request phase (clientModel, upstreamModel, sourceProtocol, accessKeyName, accessKeyGroup, requestId, etc.). See Scripting API reference 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 for the full built-in function table.

Error handling

script_error_mode

ModeBehaviorUse case
log-and-continue (default)On script error, log a warn entry and continue the request with the original bodyScript failure should not block the request
log-and-rejectOn script error, reject the request and return a gateway errorScript 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 for the full built-in function tables; Scripting config for runtime limits and configuration; Audit & security config for other security settings.