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


# 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](/en/reference/scripting-api.md) 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](/en/practices/routing-and-transform.md) 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](/en/howto/route-image-requests-to-vision-model.md)). 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](/en/practices/script-performance.md) 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!
}
```

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

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

| 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`, and `x-api-key` are removed before scripts can see them.

See [Scripting API reference](/en/reference/scripting-api.md) 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 `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 =`**:

| 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](/en/reference/scripting-config.md#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

| 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

```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](/en/reference/scripting-api.md) for the full built-in function tables; [Scripting config](/en/reference/scripting-config.md) for runtime limits and configuration; [Fix protocol translation with scripts](/en/howto/script-protocol-translate.md) for practical examples.
