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

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-route for pure routing decisions: for pure routing like "detect image/web access → switch model", use native switch-route rules — 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 under script_error_mode = log-and-continue a 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 what switch-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:

javascript
function transform(body, context) {
    // Read and modify body
    // Read context for metadata (read-only)
    return body;  // required!
}
ParameterModifiableDescription
bodyYesJSON request body or response body
contextNoRequest/response metadata (model name, headers, etc.)

Mounting

A script can be mounted on an upstream or a model:

LevelScopeUse case
UpstreamAll models passing through this upstreamProtocol adaptation, common field injection
ModelOnly this modelModel-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):

javascript
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

FieldTypeDescription
context.clientModelstringThe model name the client sees
context.upstreamModelstringThe model ID sent to the upstream
context.upstreamNamestringThe upstream group name
context.sourceProtocolstringThe client protocol (e.g. "OpenAIChatCompletions")
context.targetProtocolstringThe upstream protocol (e.g. "Anthropic")
context.streambooleanWhether the request is streaming
context.headersobjectRequest headers (sensitive headers already filtered)
context.queryobjectURL query parameters
context.requestIdstringUnique request ID
context.clientIpstringClient IP
context.accessKeyNamestringAccess key name
context.accessKeyGroupstringAccess key group

Available only in the response phase

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

Security: sensitive headers such as authorization, cookie, and x-api-key are 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:

SlotFieldRuns whenBody it seesuseModel capability
After translationrequest_transform_afterAfter protocol translationUpstream protocol formatSame-protocol switching only
Before translationrequest_transform_beforeBefore protocol translationClient's original formatSupports 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 useModel switch), then after runs. Note: once the before slot triggers an immediate useModel routing switch, the original route's after slot no longer runs (the whole pipeline is replaced with the target route's); the useModel declared in the after slot is a same-protocol deferred switch. response_transform has 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)

text
Client request → protocol translation → auto normalization → request_payload rules → upstream script → model script → cache injection → send to upstream

Request side — before slot (pre-translation)

text
Client request → upstream script → model script → (useModel swap) → protocol translation → auto normalization → request_payload rules → cache injection → send to upstream

Response side (non-streaming only)

text
Upstream response → response script → reverse protocol translation → return to client

Response scripts for streaming requests are not yet supported.

First example: override the model name

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

javascript
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 =:

OperationEffect
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):

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

json
{
  "result_body": {"model": "gpt-4o-2024-05-13", "messages": []},
  "execution_time_ms": 0.12
}

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)

You must return body on early return

javascript
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-route can express → Before
  • Pure routing decisions like "detect image/web access → switch model" (including cross-protocol) → prefer native switch-route rules, zero script overhead
  • The Before slot's context.useModel can also switch cross-protocol, but it is only worth writing a script when the switching logic is too complex for switch-route to 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.