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


# Scripting API reference

GateLLM ships a **JavaScript script engine** (QuickJS-based) for custom transforms on request and response JSON bodies. Scripts run in a sandbox with strict resource limits — safe and controllable.

This chapter is the API reference. For an intro see [Write your first script transform](/en/howto/write-script-transform.md); for config see [Scripting config](/en/reference/scripting-config.md).

## 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 | Mutable | Description |
|------|--------|------|
| `body` | yes | JSON request body or response body |
| `context` | no | Request/response metadata (model name, headers, etc.) |

## The Context object

`context` provides metadata of 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 it's a streaming request |
| `context.headers` | object | Request headers (sensitive headers filtered out) |
| `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 in the response phase only

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

### `context.useModel` route switching

```javascript
context.useModel("model-name");
```

Full route switch: base_url, protocol, and credentials change together. `body.model = "X"` only changes the request body's field, not the route — use `context.useModel` to switch across endpoints/protocols.

## The two request-script slots (before / after)

Request-side scripts are no longer one script plus a position switch, but **two independent slots**, each optionally empty and configurable together:

| Slot | Field | Run timing | Body seen | useModel capability |
|----|------|---------|-------------|---------------|
| After translation | `request_transform_after` | **after** protocol translation | upstream protocol format | same-protocol switch only |
| Before translation | `request_transform_before` | **before** protocol translation | client original format | supports cross-protocol switch |

- **`request_transform_after`**: runs after protocol translation, the body is already in upstream format. Suitable for simple field edits and upstream-dialect rewrites.
- **`request_transform_before`**: runs before protocol translation, the body is the client's original format. Suitable for detecting client input (images, search, plan-mode markers) and switching routes.
- **Both slots can be configured together**: before and after of the same route merge independently (model overrides upstream), and both scripts run — before runs first (can trigger a cross-protocol immediate `useModel` switch), after runs later. Note: once the before slot triggers an immediate `useModel` route switch, **the original route's after slot no longer runs** (the whole pipeline switches to the target route's); the after slot's declared `useModel` is a same-protocol deferred switch. `response_transform` has no slot concept and stays a single field.

## Execution order

The before and after slots each run independently; when both are configured for the same route, before runs first in the pre-translation phase and after in the post-translation phase. Below are the positions of the two slots in the pipeline.

### 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 supported yet.

## Built-in functions

### JSON Path operations

```javascript
// read a nested value
const temp = json_path(body, "generation_config.temperature");

// set a value (auto-creates intermediate objects)
set_path(body, "generation_config.max_output_tokens", 4096);

// remove a field
remove_path(body, "deprecated_field");

// add only when the field doesn't exist
add_if_absent_path(body, "temperature", 1.0);

// append to an array
append_path(body, "messages", { role: "system", content: "Be concise." });
```

### String functions

```javascript
str_len("Hello, 世界!");           // 9 (character count, not bytes)
str_contains("hello world", "world"); // true
str_replace("foo-bar", "-", "_");    // "foo_bar"
str_starts_with("hello", "he");     // true
str_trim("  hello  ");              // "hello"
```

### Regular expressions

```javascript
regex_match("abc123", "[0-9]+");     // true
regex_replace("foo123bar", "[0-9]+", "N");  // "fooNbar"
regex_extract("a=1 b=2", "([a-z]+)=");  // ["a", "b"]
```

### Encoding and hashing

```javascript
base64_encode("hello");    // "aGVsbG8="
base64_decode("aGVsbG8="); // "hello"
sha256("hello");           // "2cf24dba5fb0..."
json_encode(body.messages); // serialize to string
json_parse(jsonStr);        // parse from string
```

### Array functions

```javascript
array_sort([3, 1, 2]);                // [1, 2, 3]
array_unique([1, 2, 2, 3]);           // [1, 2, 3]
array_filter_by(items, "active", true); // keep elements with active=true
array_find(users, "name", "alice");    // find the first element with name=alice
array_pluck(users, "name");           // extract all name fields
array_sum([1.5, 2.5, 3.0]);           // 7.0
```

### Deep copy and merge

```javascript
const backup = deep_clone(body);  // independent copy
const merged = deep_merge(defaults, overrides);  // recursive merge
```

### UUID and time

```javascript
uuid();    // "550e8400-e29b-41d4-a716-..."
now();     // 1748620800 (second-level timestamp)
now_ms();  // millisecond-level
format_time(now(), "%Y-%m-%d %H:%M:%S");  // "2025-05-31 00:00:00"
```

## Runtime limits, error handling, and testing

The sandbox capability boundaries (what scripts can/can't do), the resource-limit table (operation count, heap cap, concurrent execution slots, lazy-projection semantics), `script_error_mode` error handling, common errors and troubleshooting, the test endpoint (`POST /console/api/scripts/test`), and performance advice are all in [Script runtime limits and configuration](/en/reference/scripting-config.md) — that is the single authoritative page for script config and limits; this page does not repeat them.

## FAQ

**Q: The script changed body.model, but the route didn't change.**
`body.model` only changes the request body's field, not the route. To switch routes use `context.useModel("model-name")`.

**Q: Does a response script work for streaming requests?**
Streaming-response scripts are not supported yet. Request scripts work for streaming requests, but response scripts only take effect for non-streaming.

**Q: How do I choose between Before and After script positions?**
- Only changing fields of the upstream-format body → After (default)
- Needing to detect the client's original format (e.g. different protocols have different image fields) and the judgment exceeds what `switch-route` can express → Before
- Pure routing decisions like "detect image/web-search → switch model" (including cross-protocol) → prefer the native `switch-route` rule, see [Route image-bearing requests to a vision model](/en/howto/route-image-requests-to-vision-model.md)

**Next**: [Write your first script transform](/en/howto/write-script-transform.md) for the intro structure; [Fix protocol translation with scripts](/en/howto/script-protocol-translate.md) and [Redact responses with a script](/en/howto/script-response-redact.md) for real examples; [Script runtime limits and configuration](/en/reference/scripting-config.md) for the sandbox, resource limits, error handling, and testing.
