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_transformonly 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-levelcontent, a Google upstream iscandidates), 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):
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:
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-rejectto 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:
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:
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 filterblock.type === "image", written the same way as Example 1's branch version.For simple scenarios prefer the
filter-content-typesrequest_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:
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 for the full table.
Security: sensitive headers such as
authorization,cookie, andx-api-keyare removed before scripts can see them, andcontext.responseHeadersalso filters sensitive headers.
Built-in function reference
Built-in functions commonly used for response redaction:
// 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 stringSee Scripting API reference 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-rejectto avoid un-redacted content flowing to the client when the script fails.
You must return body on early return
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.
