跳到正文

脚本 API 参考

GateLLM 内置 JavaScript 脚本引擎(基于 QuickJS),可对请求和响应的 JSON body 做自定义变换。脚本运行在沙箱中,有严格资源限制,安全且可控。

本章是 API 参考。入门见 写第一个脚本变换,配置参考见 脚本配置

基本结构

每个脚本定义一个 transform 函数,接收 bodycontext必须返回 body

javascript
function transform(body, context) {
    // 读取和修改 body
    // 读取 context 获取元数据(只读)
    return body;  // 必须!
}
参数可修改说明
bodyJSON 请求体或响应体
context请求/响应元数据(模型名、headers 等)

Context 对象

context 提供当前请求/响应的元数据,只读

请求和响应阶段均可用

字段类型说明
context.clientModelstring客户端看到的模型名
context.upstreamModelstring发送给上游的模型 ID
context.upstreamNamestring上游分组名
context.sourceProtocolstring客户端协议(如 "OpenAIChatCompletions"
context.targetProtocolstring上游协议(如 "Anthropic"
context.streamboolean是否流式请求
context.headersobject请求 headers(敏感头已过滤)
context.queryobjectURL 查询参数
context.requestIdstring唯一请求 ID
context.clientIpstring客户端 IP
context.accessKeyNamestring访问密钥名
context.accessKeyGroupstring访问密钥分组

仅响应阶段可用

字段类型说明
context.responseStatusnumber上游 HTTP 状态码
context.responseHeadersobject上游响应 headers
context.elapsedMsnumber耗时(毫秒)

安全authorizationcookiex-api-key 等敏感 headers 在脚本可见之前已被移除。

context.useModel 切换路由

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

完整路由切换:base_url、协议、凭证一起换。body.model = "X" 仅改请求体的字段,不改路由——用 context.useModel 才能跨端点/跨协议切换。

请求脚本的两个槽(before / after)

请求侧脚本不再是一个脚本加位置开关,而是两个独立槽,各自可空、可同时配置:

字段运行时机看到的 bodyuseModel 能力
翻译后request_transform_after协议翻译之后上游协议格式仅同协议切换
翻译前request_transform_before协议翻译之前客户端原始格式支持跨协议切换
  • request_transform_after:在协议转换后运行,body 已是上游格式。适合简单的字段修改、上游方言改写。
  • request_transform_before:在协议转换前运行,body 是客户端原始格式。适合需要检测客户端输入(如图片、搜索、plan 模式标记)并切换路由的场景。
  • 两个槽可同时配置:同一路由的 before 与 after 各自独立合并(model 覆盖 upstream),两个脚本都会运行——before 先跑(可触发跨协议 立即 useModel 切换),after 后跑。注意:一旦 before 槽触发了 立即 useModel 切换路由,原路由的 after 槽不再执行(整套 pipeline 换成目标路由的);after 槽声明的 useModel 是同协议 延后切换。response_transform 无槽概念,保持单字段。

执行顺序

before 槽与 after 槽各自独立运行;同一路由两者都配时,before 先在翻译前阶段跑,after 在翻译后阶段跑。下面分别给出两槽在管线中的位置。

请求侧 — after 槽(翻译后)

text
客户端请求 → 协议转换 → 自动归一化 → request_payload 规则 → 上游脚本 → 模型脚本 → 缓存注入 → 发送到上游

请求侧 — before 槽(翻译前)

text
客户端请求 → 上游脚本 → 模型脚本 → (useModel swap) → 协议转换 → 自动归一化 → request_payload 规则 → 缓存注入 → 发送到上游

响应侧(仅非流式)

text
上游响应 → 响应脚本 → 反向协议转换 → 返回客户端

流式请求的响应脚本暂不支持。

内置函数

JSON Path 操作

javascript
// 读取嵌套值
const temp = json_path(body, "generation_config.temperature");

// 设置值(自动创建中间对象)
set_path(body, "generation_config.max_output_tokens", 4096);

// 删除字段
remove_path(body, "deprecated_field");

// 仅当字段不存在时添加
add_if_absent_path(body, "temperature", 1.0);

// 向数组追加
append_path(body, "messages", { role: "system", content: "Be concise." });

字符串函数

javascript
str_len("Hello, 世界!");           // 9(字符数,非字节数)
str_contains("hello world", "world"); // true
str_replace("foo-bar", "-", "_");    // "foo_bar"
str_starts_with("hello", "he");     // true
str_trim("  hello  ");              // "hello"

正则表达式

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

编码与哈希

javascript
base64_encode("hello");    // "aGVsbG8="
base64_decode("aGVsbG8="); // "hello"
sha256("hello");           // "2cf24dba5fb0..."
json_encode(body.messages); // 序列化为字符串
json_parse(jsonStr);        // 从字符串解析

数组函数

javascript
array_sort([3, 1, 2]);                // [1, 2, 3]
array_unique([1, 2, 2, 3]);           // [1, 2, 3]
array_filter_by(items, "active", true); // 保留 active=true 的元素
array_find(users, "name", "alice");    // 找到第一个 name=alice 的元素
array_pluck(users, "name");           // 提取所有 name 字段
array_sum([1.5, 2.5, 3.0]);           // 7.0

深拷贝与合并

javascript
const backup = deep_clone(body);  // 独立副本
const merged = deep_merge(defaults, overrides);  // 递归合并

UUID 与时间

javascript
uuid();    // "550e8400-e29b-41d4-a716-..."
now();     // 1748620800(秒级时间戳)
now_ms();  // 毫秒级
format_time(now(), "%Y-%m-%d %H:%M:%S");  // "2025-05-31 00:00:00"

沙箱限制

脚本可以做什么

  • 读取和修改 body
  • 读取 context 元数据
  • 使用所有内置函数(json_path、str_、regex_、array_* 等)
  • 定义辅助函数
  • 使用标准 JavaScript(循环、条件、闭包、解构等)

脚本不能做什么

被禁止原因
console.log / print防止信息泄露
文件 I/O无文件系统访问
网络 / fetch无网络访问
import / require不能加载外部代码
eval / Function不能动态执行代码
globalThis 修改全局对象已冻结

资源限制

限制可配置
最大操作数2,000SCRIPT_MAX_OPERATIONS
解释器堆上限64 MBSCRIPT_MEMORY_LIMIT_MB(运行时创建期约束,变更需重启)
body 懒投影开启SCRIPT_LAZY_BODY(默认 truefalse 走旧整段物化分支,回滚靠显式切回;变更需重启)
最大字符串10 MB固定
最大数组/对象元素10,000固定
正则缓存256 条,5 分钟 TTL固定
栈大小1024 帧固定

ℹ️ body 视图语义script_lazy_body 开启时,body 是按需取件的懒投影视图,整份 body 不整段进 JS 堆,JS 堆占用只与脚本访问的字段挂钩。视图在 Array.isArray / instanceof / typeof / Object.keys / 解构 / 展开 / JSON.stringify / 数组方法等标准判断下与真对象一致。已知边界body.x === body.x 为假(不缓存壳),依赖同路径引用相等的脚本须改用值比较。

资源限制与配置参数的完整参考见 脚本配置

错误处理

script_error_mode

模式行为适用场景
log-and-continue(默认)脚本报错时记录 warn 日志,请求以原始 body 继续脚本失败不应阻断请求
log-and-reject脚本报错时拒绝请求,返回网关错误脚本正确性至关重要(如安全脱敏)

常见错误

错误原因解决
transform() must return a value某个 return 路径忘了返回 body每个 return 都必须返回 body
TooManyOperations超过操作数上限(默认 2000)优化循环
TypeError访问 null/undefined 的属性json_path()in 运算符检查
ReferenceError使用未定义的变量或函数检查拼写

提前返回时必须返回 body

javascript
function transform(body, context) {
    // ✅ 正确
    if (!body.messages) return body;

    // ❌ 错误:return 无值 → 脚本静默失败
    if (!body.messages) return;

    body.messages[0].content = "modified";
    return body;
}

性能建议

性能与内存

脚本开销主要来自 JSON 序列化,耗时随 body 线性增长。after 槽大 body OOM 反模式、耗时参考与写法建议见 脚本性能与内存

通过 Console API 测试

部署前先测试脚本($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
    }
  }'

响应:

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

常见问题

Q:脚本在控制台界面哪里配? 上游详情页和模型详情页都有「请求脚本」「响应脚本」输入框。也可以直接通过 Console API 设置。

Q:脚本改了 body.model,为什么路由没变?body.model 只改请求体的字段,不改路由。要切换路由用 context.useModel("模型名")

Q:流式请求的响应脚本生效吗? 暂不支持流式响应的脚本。请求脚本对流式请求有效,但响应脚本仅在非流式时生效。

Q:脚本报错了但请求正常通过? 默认的 log-and-continue 模式下,脚本报错不会阻断请求。查看 warn 日志了解报错原因。如需脚本必须成功,改用 log-and-reject

Q:context.headers 里为什么没有 Authorization? 安全设计:authorizationcookiex-api-key 等敏感 headers 在脚本可见之前已被移除,防止凭证泄露。

Q:Before 和 After 脚本位置怎么选?

  • 只改上游格式 body 的字段 → After(默认)
  • 需要检测客户端原始格式(如不同协议的图片字段不同)→ Before
  • 需要跨协议 useModel 切换 → Before
  • 同协议下「检测图片/联网→切模型」这类纯路由决策 → 不用脚本,用原生 switch-route 规则(零脚本开销首选;脚本路径因懒投影已不再整段物化 body,见 上游与模型字段

下一步写第一个脚本变换 看入门结构;用脚本做协议翻译修正用脚本做响应脱敏 看实战示例;脚本配置 看运行限制与配置。