Write your first script transform
GateLLM has a built-in JavaScript scripting engine (based on QuickJS) that can apply custom transforms to the JSON bodies of requests and responses. Scripts run in a sandbox with strict resource limits — safe and controllable.
This chapter walks you through writing your first script: basic structure → mounting → configuration → Context API overview. See Scripting API reference for the full API tables.
When to use scripts
request_payload covers about 90% of simple scenarios (simple field add/remove/modify), switch-route handles pure routing decisions (detect image/web access → switch model), and scripts handle the remaining complex logic (cross-field computation, response redaction). See Request rewrite vs routing: which to pick for the full decision table and selection guide.
⚠️ Prefer
switch-routefor pure routing decisions: for pure routing like "detect image/web access → switch model", use nativeswitch-routerules — zero script overhead and supports cross-protocol switching (see Route image-bearing requests to a vision model). The script path already enables lazy projection by default, so it no longer OOMs from materializing the whole body; but underscript_error_mode = log-and-continuea script error is silently skipped, so scripts should be written not to throw. Only use a before-hook script (which reads the client's original format) when the decision logic goes beyond whatswitch-route's structural checks (field existence / small-value equality) can express. See Script performance & memory for the full anti-patterns and latency references.
Quick start
Basic structure
Each script defines a transform function that receives body and context, and must return body:
function transform(body, context) {
// Read and modify body
// Read context for metadata (read-only)
return body; // required!
}| Parameter | Modifiable | Description |
|---|---|---|
body | Yes | JSON request body or response body |
context | No | Request/response metadata (model name, headers, etc.) |
Mounting
A script can be mounted on an upstream or a model:
| Level | Scope | Use case |
|---|---|---|
| Upstream | All models passing through this upstream | Protocol adaptation, common field injection |
| Model | Only this model | Model-specific adjustments, parameter tuning |
Configuration
Inline script (short scripts written directly in the input box) — Console → model details (or upstream) → Request/response script → paste into the corresponding slot (request_transform_after / request_transform_before / response_transform):
function transform(body, context) { body.temperature = 0.7; return body; }File script (larger scripts referencing an external .js file): the console's script editor is inline-only and has no "select file" control — a {file} reference cannot be created in the UI and can only be set through the Console API (PUT upstream / model) with request_transform_before: {"file": "scripts/my-transform.js"}; the UI preserves an already-set {file} reference as-is until you type inline content into that slot to overwrite it.
The Context object
context provides metadata about the current request/response, read-only:
Available in both request and response phases
| Field | Type | Description |
|---|---|---|
context.clientModel | string | The model name the client sees |
context.upstreamModel | string | The model ID sent to the upstream |
context.upstreamName | string | The upstream group name |
context.sourceProtocol | string | The client protocol (e.g. "OpenAIChatCompletions") |
context.targetProtocol | string | The upstream protocol (e.g. "Anthropic") |
context.stream | boolean | Whether the request is streaming |
context.headers | object | Request headers (sensitive headers already filtered) |
context.query | object | URL query parameters |
context.requestId | string | Unique request ID |
context.clientIp | string | Client IP |
context.accessKeyName | string | Access key name |
context.accessKeyGroup | string | Access key group |
Available only 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) |
Security: sensitive headers such as
authorization,cookie, andx-api-keyare removed before scripts can see them.
See Scripting API reference for the full field tables.
Two request-side slots (before / after)
The request-side script is no longer a single script plus a position switch, but two independent slots, each optional and both configurable at the same time:
| Slot | Field | Runs when | Body it sees | useModel capability |
|---|---|---|---|---|
| After translation | request_transform_after | After protocol translation | Upstream protocol format | Same-protocol switching only |
| Before translation | request_transform_before | Before protocol translation | Client's original format | Supports cross-protocol switching |
request_transform_after: runs after protocol conversion; the body is already in the upstream format. Good for simple field changes and upstream dialect rewriting.request_transform_before: runs before protocol conversion; the body is the client's original format. Good for scenarios that need to detect client input (such as images, search, plan-mode markers) and switch routing.- Both slots can be configured at once: the before and after slots of the same route merge independently (model overrides upstream), and both scripts run — before runs first (can trigger an immediate cross-protocol
useModelswitch), then after runs. Note: once the before slot triggers an immediateuseModelrouting switch, the original route's after slot no longer runs (the whole pipeline is replaced with the target route's); theuseModeldeclared in the after slot is a same-protocol deferred switch.response_transformhas no slot concept and stays a single field.
Execution order
The before slot and after slot each run independently; when a route configures both, before runs first in the pre-translation phase and after runs in the post-translation phase. The two slots' positions in the pipeline are given below.
Request side — after slot (post-translation)
Client request → protocol translation → auto normalization → request_payload rules → upstream script → model script → cache injection → send to upstreamRequest side — before slot (pre-translation)
Client request → upstream script → model script → (useModel swap) → protocol translation → auto normalization → request_payload rules → cache injection → send to upstreamResponse side (non-streaming only)
Upstream response → response script → reverse protocol translation → return to clientResponse scripts for streaming requests are not yet supported.
First example: override the model name
function transform(body, context) {
body.model = context.upstreamModel;
return body;
}Mount on the request_transform_after field of a model or upstream. Before the request is sent to the upstream, the body's model field is replaced with the upstream's real model ID.
Switching models: context.useModel
A script can dynamically switch the model a request is routed to:
function transform(body, context) {
// Detect multimodal input (images), switch to a multimodal model
const msgs = body.messages || [];
const hasImage = msgs.some(m =>
Array.isArray(m.content) && m.content.some(c => c.type === "image_url"));
if (hasImage) {
context.useModel("gpt-4o-vision");
}
return body;
}context.useModel vs body.model =:
| Operation | Effect |
|---|---|
body.model = "X" | Only changes the request body's model field; routing is unchanged |
context.useModel("X") | Full routing switch (base_url, protocol, credentials all change together) |
Use body.model to change the model name within the same endpoint; use context.useModel for cross-endpoint / cross-protocol switching.
Testing via the Console API
Test the script before deploying (see Script configuration → Script testing for how to get $CONSOLE_TOKEN):
curl -X POST http://localhost:7890/console/api/scripts/test \
-H "Authorization: Bearer $CONSOLE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"script": "function transform(body, context) { body.model = context.upstreamModel; return body; }",
"body": {"model": "gpt-4o", "messages": []},
"context": {
"clientModel": "gpt-4o",
"upstreamModel": "gpt-4o-2024-05-13",
"upstreamName": "openai",
"sourceProtocol": "OpenAIChatCompletions",
"targetProtocol": "OpenAIChatCompletions",
"stream": false
}
}'Response:
{
"result_body": {"model": "gpt-4o-2024-05-13", "messages": []},
"execution_time_ms": 0.12
}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) |
You must return body on early return
function transform(body, context) {
// ✅ Correct
if (!body.messages) return body;
// ❌ Wrong: return with no value → the script silently fails
if (!body.messages) return;
body.messages[0].content = "modified";
return body;
}FAQ
Q: Where do I configure scripts in the console? Both the upstream details page and the model details page have "Request script" and "Response script" input boxes. You can also set them directly via the Console API.
Q: The script changed body.model, why didn't the routing change?body.model only changes the request body's field, not the routing. To switch routing use context.useModel("model name").
Q: Does the response script work for streaming requests? Streaming response scripts are not yet supported. Request scripts work for streaming requests, but response scripts only take effect for non-streaming requests.
Q: The script errored but the request passed normally? Under the default log-and-continue mode, a script error does not block the request. Check the warn log for the cause. If the script must succeed, switch to log-and-reject.
Q: Why isn't Authorization in context.headers? 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 choose between the Before and After script slots?
- Only changing fields of the upstream-format body → After (default)
- Need to detect the client's original format (e.g. different protocols have different image fields) and the decision goes beyond what
switch-routecan express → Before - Pure routing decisions like "detect image/web access → switch model" (including cross-protocol) → prefer native
switch-routerules, zero script overhead - The Before slot's
context.useModelcan also switch cross-protocol, but it is only worth writing a script when the switching logic is too complex forswitch-routeto express
Next: Scripting API reference for the full built-in function tables; Scripting config for runtime limits and configuration; Fix protocol translation with scripts for practical examples.
