This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new d3f343d04 feat(ai): support configurable request JSON library (#13386)
d3f343d04 is described below
commit d3f343d04e0d9cdc1b6a72559a4ec2c601cd3923
Author: Nic <[email protected]>
AuthorDate: Thu May 21 13:56:25 2026 +0800
feat(ai): support configurable request JSON library (#13386)
---
.devcontainer/Dockerfile | 2 +-
Makefile | 11 +-
apisix-master-0.rockspec | 2 +
apisix/cli/config.lua | 1 +
apisix/cli/schema.lua | 4 +
apisix/core/request.lua | 4 +-
apisix/core/request_json.lua | 68 ++++++++++
apisix/plugins/ai-transport/auth-aws.lua | 4 +-
apisix/plugins/ai-transport/http.lua | 3 +-
ci/common.sh | 13 +-
ci/linux_apisix_current_luarocks_runner.sh | 4 +
conf/config.yaml.example | 4 +
docker/debian-dev/Dockerfile | 8 ++
docs/en/latest/plugins/ai-proxy.md | 23 ++++
docs/zh/latest/plugins/ai-proxy.md | 23 ++++
t/chaos/utils/Dockerfile | 4 +-
t/core/config-default.t | 2 +
t/core/request.t | 205 ++++++++++++++++++++++++++++-
t/plugin/ai-proxy-multi.t | 4 +-
t/plugin/ai-proxy.t | 4 +-
utils/install-dependencies.sh | 10 +-
utils/install-rust-toolchain.sh | 149 +++++++++++++++++++++
22 files changed, 534 insertions(+), 18 deletions(-)
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 4402db7e8..9ae08b865 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -21,7 +21,7 @@ RUN apt update && export DEBIAN_FRONTEND=noninteractive \
&& apt install -y sudo git make gcc tini
COPY Makefile .requirements apisix-master-0.rockspec ./
-COPY utils/install-dependencies.sh utils/linux-install-luarocks.sh utils/
+COPY utils/install-dependencies.sh utils/install-rust-toolchain.sh
utils/linux-install-luarocks.sh utils/
RUN make install-runtime
diff --git a/Makefile b/Makefile
index 71ab7df1e..0e5bcc719 100644
--- a/Makefile
+++ b/Makefile
@@ -136,7 +136,16 @@ deps: install-runtime
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL)
variables.OPENSSL_LIBDIR $(addprefix $(ENV_OPENSSL_PREFIX), /lib); \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL)
variables.OPENSSL_INCDIR $(addprefix $(ENV_OPENSSL_PREFIX), /include); \
$(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL)
variables.YAML_DIR $(ENV_LIBYAML_INSTALL_PREFIX); \
- $(ENV_LUAROCKS) install apisix-master-0.rockspec --tree deps
--only-deps $(ENV_LUAROCKS_SERVER_OPT); \
+ rustup_home=$${RUSTUP_HOME:-}; \
+ cargo_home=$${CARGO_HOME:-}; \
+ if [ -z "$$rustup_home" ]; then \
+ if [ -d /usr/local/rustup ] && [ -w /usr/local/rustup
]; then rustup_home=/usr/local/rustup; else rustup_home=$$HOME/.rustup; fi; \
+ fi; \
+ if [ -z "$$cargo_home" ]; then \
+ if [ -d /usr/local/cargo ] && [ -w /usr/local/cargo ];
then cargo_home=/usr/local/cargo; else cargo_home=$$HOME/.cargo; fi; \
+ fi; \
+ RUSTUP_HOME=$$rustup_home CARGO_HOME=$$cargo_home
PATH=$$cargo_home/bin:$$PATH \
+ $(ENV_LUAROCKS) install apisix-master-0.rockspec --tree
deps --only-deps $(ENV_LUAROCKS_SERVER_OPT); \
else \
$(call func_echo_warn_status, "WARNING: You're not using
LuaRocks 3.x; please remove the luarocks and reinstall it via
https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh");
\
exit 1; \
diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec
index c9d9c73be..2beb608ba 100644
--- a/apisix-master-0.rockspec
+++ b/apisix-master-0.rockspec
@@ -86,6 +86,8 @@ dependencies = {
"api7-lua-resty-aws == 2.0.2-1",
"multipart = 0.5.11-1",
"luautf8 = 0.2.0-1",
+ "lua-qjson = 0.1.0-1",
+ "api7-lua-resty-simdjson = 0.1.1-1",
}
build = {
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index 158d2d602..8ac1e567b 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -29,6 +29,7 @@ local _M = {
enable_server_tokens = true,
extra_lua_path = "",
extra_lua_cpath = "",
+ request_body_json_lib = "simdjson",
proxy_cache = {
cache_ttl = "10s",
zones = {
diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua
index 8a70bda8b..2d9ed30a2 100644
--- a/apisix/cli/schema.lua
+++ b/apisix/cli/schema.lua
@@ -92,6 +92,10 @@ local config_schema = {
},
}
},
+ request_body_json_lib = {
+ enum = {"cjson", "simdjson", "qjson"},
+ default = "simdjson",
+ },
proxy_cache = {
type = "object",
properties = {
diff --git a/apisix/core/request.lua b/apisix/core/request.lua
index ef5bcb6bb..f999a2cda 100644
--- a/apisix/core/request.lua
+++ b/apisix/core/request.lua
@@ -21,7 +21,7 @@
local lfs = require("lfs")
local log = require("apisix.core.log")
-local json = require("apisix.core.json")
+local request_json = require("apisix.core.request_json")
local io = require("apisix.core.io")
local multipart = require("multipart")
local core_str = require("apisix.core.string")
@@ -371,7 +371,7 @@ local function get_request_body_table(ctx, content_type)
if not body then
return nil, "could not get body: " .. (body_err or "request body
is empty")
end
- result, err = json.decode(body)
+ result, err = request_json.decode(body)
if not result then
return nil, "could not parse JSON request body: " .. (err or
"invalid JSON")
end
diff --git a/apisix/core/request_json.lua b/apisix/core/request_json.lua
new file mode 100644
index 000000000..90ec9a058
--- /dev/null
+++ b/apisix/core/request_json.lua
@@ -0,0 +1,68 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+local config_local = require("apisix.core.config_local")
+local core_json = require("apisix.core.json")
+local qjson = require("qjson")
+local simdjson = require("resty.simdjson")
+
+
+local simdjson_parser, simdjson_err = simdjson.new()
+assert(simdjson_parser, simdjson_err)
+local configured_name
+
+local _M = {}
+
+
+function _M.decode(str)
+ if not configured_name then
+ configured_name =
config_local.local_conf().apisix.request_body_json_lib
+ end
+
+ local name = configured_name
+ if name == "cjson" then
+ return core_json.decode(str)
+ end
+
+ if name == "simdjson" then
+ return simdjson_parser:decode(str)
+ end
+
+ local decoded, err = qjson.decode(str)
+ if not decoded then
+ return nil, err
+ end
+
+ return qjson.materialize(decoded)
+end
+
+
+function _M.encode(data)
+ if not configured_name then
+ configured_name =
config_local.local_conf().apisix.request_body_json_lib
+ end
+
+ if configured_name == "qjson" then
+ return qjson.encode(data)
+ end
+
+ -- simdjson encode is slower than cjson, so simdjson mode only uses it for
decode.
+ return core_json.encode(data)
+end
+
+
+return _M
diff --git a/apisix/plugins/ai-transport/auth-aws.lua
b/apisix/plugins/ai-transport/auth-aws.lua
index bd8529a25..cd2c8600a 100644
--- a/apisix/plugins/ai-transport/auth-aws.lua
+++ b/apisix/plugins/ai-transport/auth-aws.lua
@@ -20,7 +20,7 @@
require("resty.aws.config") -- reads env vars before init
local aws = require("resty.aws")
-local core = require("apisix.core")
+local request_json = require("apisix.core.request_json")
local signer = require("resty.aws.request.sign")
local ngx = ngx
local ngx_escape_uri = ngx.escape_uri
@@ -81,7 +81,7 @@ function _M.sign_request(params, aws_conf, region)
-- Serialize body to JSON string (SigV4 signs the exact bytes)
if type(params.body) == "table" then
- local body_str, err = core.json.encode(params.body)
+ local body_str, err = request_json.encode(params.body)
if not body_str then
return "failed to encode body: " .. (err or "")
end
diff --git a/apisix/plugins/ai-transport/http.lua
b/apisix/plugins/ai-transport/http.lua
index bf8a06c1a..242efa1ad 100644
--- a/apisix/plugins/ai-transport/http.lua
+++ b/apisix/plugins/ai-transport/http.lua
@@ -19,6 +19,7 @@
-- Provides HTTP client lifecycle management for AI provider requests.
local core = require("apisix.core")
+local request_json = require("apisix.core.request_json")
local http = require("resty.http")
local ngx_now = ngx.now
local pairs = pairs
@@ -107,7 +108,7 @@ function _M.request(params, timeout)
req_json = params.body
else
local err
- req_json, err = core.json.encode(params.body)
+ req_json, err = request_json.encode(params.body)
if not req_json then
httpc:close()
return nil, "encode body: " .. (err or "unknown"), {
diff --git a/ci/common.sh b/ci/common.sh
index b99819b5e..30ea4a46f 100644
--- a/ci/common.sh
+++ b/ci/common.sh
@@ -17,6 +17,11 @@
set -ex
+_APISIX_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/utils"
+# shellcheck source=../utils/install-rust-toolchain.sh
+. "${_APISIX_UTILS_DIR}/install-rust-toolchain.sh"
+unset _APISIX_UTILS_DIR
+
export_version_info() {
source ./.requirements
}
@@ -32,6 +37,10 @@ export_or_prefix() {
create_lua_deps() {
echo "Create lua deps"
+ export RUSTUP_HOME="${RUSTUP_HOME:-/usr/local/rustup}"
+ export CARGO_HOME="${CARGO_HOME:-/usr/local/cargo}"
+ export PATH="${CARGO_HOME}/bin:${PATH}"
+
make deps
# just for jwt-auth test
@@ -177,13 +186,15 @@ GRPC_SERVER_EXAMPLE_VER=20210819
linux_get_dependencies () {
apt update
- apt install -y cpanminus build-essential libncurses5-dev libreadline-dev
libssl-dev perl libpcre3 libpcre3-dev libpcre2-dev xz-utils redis-tools
+ apt install -y cargo cpanminus build-essential libncurses5-dev
libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libpcre2-dev xz-utils
redis-tools
+ install_rust_toolchain
apt remove -y curl
apt-get install -y libyaml-dev
wget
https://github.com/mikefarah/yq/releases/download/3.4.1/yq_linux_amd64 -O
/usr/bin/yq && sudo chmod +x /usr/bin/yq
# install curl with http3 support
install_curl
+
}
function start_grpc_server_example() {
diff --git a/ci/linux_apisix_current_luarocks_runner.sh
b/ci/linux_apisix_current_luarocks_runner.sh
index 94abcd466..8a8f44514 100755
--- a/ci/linux_apisix_current_luarocks_runner.sh
+++ b/ci/linux_apisix_current_luarocks_runner.sh
@@ -43,6 +43,10 @@ script() {
sudo rm -rf /usr/local/share/lua/5.1/apisix
+ export RUSTUP_HOME="${RUSTUP_HOME:-/usr/local/rustup}"
+ export CARGO_HOME="${CARGO_HOME:-/usr/local/cargo}"
+ export PATH="${CARGO_HOME}/bin:${PATH}"
+
# install APISIX with local version
luarocks install apisix-master-0.rockspec --only-deps > build.log 2>&1 ||
(cat build.log && exit 1)
luarocks make apisix-master-0.rockspec > build.log 2>&1 || (cat build.log
&& exit 1)
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 39493fb67..482eb13be 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -44,6 +44,10 @@ apisix:
enable_server_tokens: true # If true, show APISIX version in the
`Server` response header.
extra_lua_path: "" # Extend lua_package_path to load
third-party code.
extra_lua_cpath: "" # Extend lua_package_cpath to load
third-party code.
+ request_body_json_lib: simdjson # JSON library used to decode request
bodies parsed by core.request.
+ # Also controls AI upstream request
body encoding.
+ # The value is resolved per worker.
Reload or restart workers after changing it.
+ # Can be cjson, simdjson, or qjson.
qjson is experimental.
# lua_module_hook: "my_project.my_hook" # Hook module used to inject
third-party code into APISIX.
proxy_cache: # Proxy Caching configuration
diff --git a/docker/debian-dev/Dockerfile b/docker/debian-dev/Dockerfile
index ff906d551..acfe8eb14 100644
--- a/docker/debian-dev/Dockerfile
+++ b/docker/debian-dev/Dockerfile
@@ -21,6 +21,9 @@ ARG CODE_PATH
ENV DEBIAN_FRONTEND=noninteractive
ENV ENV_INST_LUADIR=/usr/local/apisix
+ENV RUSTUP_HOME=/usr/local/rustup
+ENV CARGO_HOME=/usr/local/cargo
+ENV PATH=/usr/local/cargo/bin:$PATH
COPY ${CODE_PATH} /apisix
@@ -32,7 +35,12 @@ RUN set -x \
make \
git \
sudo \
+ ca-certificates \
+ curl \
+ gcc \
+ g++ \
libyaml-dev \
+ && bash -c '. ./utils/install-rust-toolchain.sh && install_rust_toolchain'
\
&& ls -al \
&& make deps \
&& mkdir -p ${ENV_INST_LUADIR} \
diff --git a/docs/en/latest/plugins/ai-proxy.md
b/docs/en/latest/plugins/ai-proxy.md
index 7996cc25a..ab253df4b 100644
--- a/docs/en/latest/plugins/ai-proxy.md
+++ b/docs/en/latest/plugins/ai-proxy.md
@@ -42,6 +42,29 @@ The `ai-proxy` Plugin simplifies access to LLM and embedding
models by transform
In addition, the Plugin also supports logging LLM request information in the
access log, such as token usage, model, time to the first response, and more.
These log entries are also consumed by logging plugins such as `http-logger`
and `kafka-logger`. These options do not affect `error.log`.
+## Request Body JSON Library
+
+APISIX uses `apisix.request_body_json_lib` to select the JSON library for
request body parsing through `core.request.get_request_body_table`. This is a
core request helper, so the setting affects every Plugin that reads JSON
request bodies through this API, including `ai-proxy` and other AI Plugins. It
also controls JSON encoding for AI upstream request bodies.
+
+```yaml title="conf/config.yaml"
+apisix:
+ request_body_json_lib: simdjson
+```
+
+Valid values are `cjson`, `simdjson`, and `qjson`. The default is `simdjson`.
When `simdjson` is configured, APISIX uses `simdjson` to decode request bodies
and `cjson` to encode AI upstream request bodies. `qjson` is available as an
experimental option for users who want to explicitly opt in to the
highest-throughput path.
+
+The value is resolved per worker. Reload or restart APISIX workers after
changing it.
+
+The following benchmark data was measured with large OpenAI chat completion
payloads and `post_arg.model` route matching, which triggers request body JSON
parsing during route matching. The `qjson` result uses qjson for both request
body decode and AI upstream request body encode.
+
+| Payload | `cjson` QPS | `simdjson` QPS | `qjson` QPS | `simdjson` vs `cjson`
| `qjson` vs `cjson` |
+|---------|-------------|----------------|-------------|------------------------|--------------------|
+| 1 MB | 173 | 250 | 604 | 1.45x
| 3.41x |
+| 5 MB | 38 | 48-54 | 146-147 | 1.24x
| 3.85x |
+| 10 MB | 17.4 | 27.4 | 77.9 | 1.58x
| 4.48x |
+
+Use `simdjson` when you want to accelerate request body decoding while keeping
`cjson` encoding semantics for AI upstream request bodies. `qjson` showed the
best throughput in this benchmark, but it is experimental and should be
selected explicitly after evaluating compatibility for your workloads.
+
## Request Format
| Name | Type | Required | Description
|
diff --git a/docs/zh/latest/plugins/ai-proxy.md
b/docs/zh/latest/plugins/ai-proxy.md
index 425961251..2d49c0c65 100644
--- a/docs/zh/latest/plugins/ai-proxy.md
+++ b/docs/zh/latest/plugins/ai-proxy.md
@@ -42,6 +42,29 @@ import TabItem from '@theme/TabItem';
此外,该插件还支持在访问日志中记录 LLM 请求信息,如令牌使用量、模型、首次响应时间等。这些日志条目也会被
`http-logger`、`kafka-logger` 等日志插件消费。这些选项不影响 `error.log`。
+## 请求体 JSON 库
+
+APISIX 使用 `apisix.request_body_json_lib` 选择
`core.request.get_request_body_table` 解析请求体时使用的 JSON
库。该入口是核心请求辅助接口,因此该配置会影响所有通过此接口读取 JSON 请求体的插件,包括 `ai-proxy` 和其他 AI 插件。该配置也会控制 AI
上游请求体的 JSON 编码。
+
+```yaml title="conf/config.yaml"
+apisix:
+ request_body_json_lib: simdjson
+```
+
+有效值为 `cjson`、`simdjson` 和 `qjson`,默认值为 `simdjson`。当配置为 `simdjson` 时,APISIX 使用
`simdjson` 解码请求体,并使用 `cjson` 编码 AI 上游请求体。`qjson`
作为实验性选项提供,适合希望显式选择最高吞吐路径的用户自行评估后启用。
+
+该配置值按 worker 解析。修改后需要 reload 或 restart APISIX worker 才会生效。
+
+以下性能数据来自使用大 OpenAI chat completion 请求体和 `post_arg.model` 路由匹配的
benchmark,因此路由匹配阶段也会触发请求体 JSON 解析。`qjson` 结果表示请求体解码和 AI 上游请求体编码都使用 qjson。
+
+| 请求体大小 | `cjson` QPS | `simdjson` QPS | `qjson` QPS | `simdjson` 相对 `cjson` |
`qjson` 相对 `cjson` |
+|------------|-------------|----------------|-------------|--------------------------|----------------------|
+| 1 MB | 173 | 250 | 604 | 1.45x
| 3.41x |
+| 5 MB | 38 | 48-54 | 146-147 | 1.24x
| 3.85x |
+| 10 MB | 17.4 | 27.4 | 77.9 | 1.58x
| 4.48x |
+
+如果希望加速请求体解码,同时保持 AI 上游请求体使用 `cjson` 编码语义,可以使用 `simdjson`。`qjson` 在该 benchmark
中吞吐最高,但仍是实验性选项,建议用户基于自身负载评估兼容性后显式启用。
+
## 请求格式
| 名称 | 类型 | 必选项 | 描述 |
diff --git a/t/chaos/utils/Dockerfile b/t/chaos/utils/Dockerfile
index 5d5ba6dac..99c0c733d 100644
--- a/t/chaos/utils/Dockerfile
+++ b/t/chaos/utils/Dockerfile
@@ -27,12 +27,15 @@ RUN set -x \
&& apk add --no-cache --virtual .builddeps \
automake \
autoconf \
+ cargo \
+ g++ \
libtool \
pkgconfig \
cmake \
git \
openldap-dev \
pcre-dev \
+ rust \
sudo \
&& cd apisix \
&& git config --global url.https://github.com/.insteadOf git://github.com/
\
@@ -73,4 +76,3 @@ EXPOSE 9080 9180 9443
CMD ["sh", "-c", "/usr/bin/apisix init && /usr/bin/apisix init_etcd &&
/usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'"]
STOPSIGNAL SIGQUIT
-
diff --git a/t/core/config-default.t b/t/core/config-default.t
index f19392d24..1fe3c0a2b 100644
--- a/t/core/config-default.t
+++ b/t/core/config-default.t
@@ -31,6 +31,7 @@ __DATA__
local config = require("apisix.core").config.local_conf()
ngx.say("node_listen: ", config.apisix.node_listen)
+ ngx.say("request_body_json_lib: ",
config.apisix.request_body_json_lib)
ngx.say("stream_proxy: ", encode_json(config.apisix.stream_proxy))
ngx.say("admin_key: ",
encode_json(config.deployment.admin.admin_key))
}
@@ -39,6 +40,7 @@ __DATA__
GET /t
--- response_body
node_listen: 1984
+request_body_json_lib: simdjson
stream_proxy: {"tcp":[9100]}
admin_key: null
diff --git a/t/core/request.t b/t/core/request.t
index 14c2e901e..fb72d075e 100644
--- a/t/core/request.t
+++ b/t/core/request.t
@@ -494,17 +494,20 @@ apisix
=== TEST 17: get_json_request_body_table caches result and re-decodes after
set_body_data
+--- yaml_config
+apisix:
+ request_body_json_lib: cjson
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
- local json = require("apisix.core.json")
+ local request_json = require("apisix.core.request_json")
ngx.ctx.api_ctx = {}
local decode_count = 0
- local orig_decode = json.decode
- json.decode = function(str)
+ local orig_decode = request_json.decode
+ request_json.decode = function(str)
decode_count = decode_count + 1
return orig_decode(str)
end
@@ -525,7 +528,7 @@ apisix
-- cache cleared, must re-decode
local t4 = core.request.get_json_request_body_table()
- json.decode = orig_decode
+ request_json.decode = orig_decode
ngx.say("after set_body model: ", t4 and t4.model)
ngx.say("decode_count: ", decode_count)
@@ -542,3 +545,197 @@ same table: true
decode_count: 1
after set_body model: claude
decode_count: 2
+
+
+
+=== TEST 18: request_json selects configured JSON library
+--- config
+ location /t {
+ content_by_lua_block {
+ local config_local = require("apisix.core.config_local")
+ local orig_local_conf = config_local.local_conf
+ local orig_qjson = package.loaded["qjson"]
+ local orig_preload_qjson = package.preload["qjson"]
+ local orig_simdjson = package.loaded["resty.simdjson"]
+ local orig_preload_simdjson = package.preload["resty.simdjson"]
+ local orig_request_json =
package.loaded["apisix.core.request_json"]
+
+ package.loaded["qjson"] = {
+ decode = function()
+ return {lib = "qjson", lazy = true}
+ end,
+ materialize = function(data)
+ data.lazy = false
+ return data
+ end,
+ encode = function(data)
+ return "qjson:" .. data.lib
+ end,
+ }
+
+ package.loaded["resty.simdjson"] = {
+ new = function()
+ return {
+ decode = function()
+ return {lib = "simdjson"}
+ end,
+ }
+ end,
+ }
+
+ local function load_with(lib)
+ config_local.local_conf = function()
+ return {apisix = {request_body_json_lib = lib}}
+ end
+ package.loaded["apisix.core.request_json"] = nil
+ return require("apisix.core.request_json")
+ end
+
+ local request_json = load_with("qjson")
+ local decoded = request_json.decode("{}")
+ local encoded = request_json.encode({lib = "body"})
+ ngx.say("qjson decode: ", decoded.lib)
+ ngx.say("qjson materialized: ", not decoded.lazy)
+ ngx.say("qjson encode: ", encoded)
+
+ request_json = load_with("simdjson")
+ decoded = request_json.decode("{}")
+ encoded = request_json.encode({lib = "body"})
+ ngx.say("simdjson decode: ", decoded.lib)
+ ngx.say("simdjson encode: ", encoded)
+
+ request_json = load_with("cjson")
+ decoded = request_json.decode('{"lib":"cjson"}')
+ encoded = request_json.encode({lib = "body"})
+ ngx.say("cjson decode: ", decoded.lib)
+ ngx.say("cjson encode: ", encoded)
+
+ config_local.local_conf = orig_local_conf
+ package.loaded["apisix.core.request_json"] = orig_request_json
+ package.loaded["qjson"] = orig_qjson
+ package.preload["qjson"] = orig_preload_qjson
+ package.loaded["resty.simdjson"] = orig_simdjson
+ package.preload["resty.simdjson"] = orig_preload_simdjson
+ }
+ }
+--- response_body
+qjson decode: qjson
+qjson materialized: true
+qjson encode: qjson:body
+simdjson decode: simdjson
+simdjson encode: {"lib":"body"}
+cjson decode: cjson
+cjson encode: {"lib":"body"}
+
+
+
+=== TEST 19: simdjson preserves empty arrays for cjson encoding
+--- yaml_config
+apisix:
+ request_body_json_lib: simdjson
+--- config
+ location /t {
+ content_by_lua_block {
+ local core_json = require("apisix.core.json")
+ local request_json = require("apisix.core.request_json")
+
+ local decoded =
request_json.decode('{"messages":[],"metadata":{"tags":[]}}')
+ local encoded = request_json.encode(decoded)
+ local round_trip = core_json.decode(encoded)
+
+ ngx.say("messages array: ", getmetatable(round_trip.messages) ==
core_json.array_mt)
+ ngx.say("tags array: ", getmetatable(round_trip.metadata.tags) ==
core_json.array_mt)
+ }
+ }
+--- response_body
+messages array: true
+tags array: true
+
+
+
+=== TEST 20: ai transport encoders use request_json
+--- config
+ location /t {
+ content_by_lua_block {
+ local request_json = require("apisix.core.request_json")
+ local orig_encode = request_json.encode
+ local orig_request_json =
package.loaded["apisix.core.request_json"]
+ local orig_http = package.loaded["resty.http"]
+ local orig_aws_config = package.loaded["resty.aws.config"]
+ local orig_aws = package.loaded["resty.aws"]
+ local orig_sign = package.loaded["resty.aws.request.sign"]
+ local orig_transport =
package.loaded["apisix.plugins.ai-transport.http"]
+ local orig_auth_aws =
package.loaded["apisix.plugins.ai-transport.auth-aws"]
+ request_json.encode = function()
+ return "encoded-by-request-json"
+ end
+
+ package.loaded["resty.http"] = {
+ new = function()
+ return {
+ set_timeout = function() end,
+ connect = function() return true end,
+ request = function(_, params)
+ ngx.say("http body: ", params.body)
+ return {headers = {}, status = 200}
+ end,
+ close = function() end,
+ }
+ end,
+ }
+
+ package.loaded["apisix.plugins.ai-transport.http"] = nil
+ local transport = require("apisix.plugins.ai-transport.http")
+ local res, err = transport.request({
+ host = "127.0.0.1",
+ port = 80,
+ path = "/",
+ body = {model = "test"},
+ }, 1000)
+ if not res then
+ ngx.say(err)
+ end
+
+ package.loaded["resty.aws.config"] = {}
+ package.loaded["resty.aws"] = function()
+ return {
+ Credentials = function(_, opts)
+ return opts
+ end,
+ }
+ end
+ package.loaded["resty.aws.request.sign"] = function(_, req)
+ ngx.say("aws body: ", req.body)
+ return {headers = {Authorization = "signed"}}
+ end
+
+ package.loaded["apisix.plugins.ai-transport.auth-aws"] = nil
+ local auth_aws = require("apisix.plugins.ai-transport.auth-aws")
+ local sign_err = auth_aws.sign_request({
+ method = "POST",
+ host = "bedrock-runtime.us-east-1.amazonaws.com",
+ port = 443,
+ path = "/model/test/converse",
+ headers = {},
+ body = {model = "test"},
+ }, {
+ access_key_id = "ak",
+ secret_access_key = "sk",
+ }, "us-east-1")
+ if sign_err then
+ ngx.say(sign_err)
+ end
+
+ request_json.encode = orig_encode
+ package.loaded["resty.http"] = orig_http
+ package.loaded["resty.aws.config"] = orig_aws_config
+ package.loaded["resty.aws"] = orig_aws
+ package.loaded["resty.aws.request.sign"] = orig_sign
+ package.loaded["apisix.core.request_json"] = orig_request_json
+ package.loaded["apisix.plugins.ai-transport.http"] = orig_transport
+ package.loaded["apisix.plugins.ai-transport.auth-aws"] =
orig_auth_aws
+ }
+ }
+--- response_body
+http body: encoded-by-request-json
+aws body: encoded-by-request-json
diff --git a/t/plugin/ai-proxy-multi.t b/t/plugin/ai-proxy-multi.t
index c2ecdcd4f..081184faa 100644
--- a/t/plugin/ai-proxy-multi.t
+++ b/t/plugin/ai-proxy-multi.t
@@ -260,8 +260,8 @@ qr/\{ "content": "1 \+ 1 = 2\.", "role": "assistant" \}/
GET /anything
{}"messages": [ { "role": "system", "cont
--- error_code: 400
---- response_body
-{"message":"could not parse JSON request body: Expected the end but found
T_STRING at character 3"}
+--- response_body eval
+qr/^\{"message":"could not parse JSON request body: .+"\}\n?$/
diff --git a/t/plugin/ai-proxy.t b/t/plugin/ai-proxy.t
index e73e41a86..0a18db577 100644
--- a/t/plugin/ai-proxy.t
+++ b/t/plugin/ai-proxy.t
@@ -234,8 +234,8 @@ qr/\{ "content": "1 \+ 1 = 2\.", "role": "assistant" \}/
GET /anything
{}"messages": [ { "role": "system", "cont
--- error_code: 400
---- response_body
-{"message":"could not parse JSON request body: Expected the end but found
T_STRING at character 3"}
+--- response_body eval
+qr/^\{"message":"could not parse JSON request body: .+"\}\n?$/
diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh
index 57517997c..74c646b8f 100755
--- a/utils/install-dependencies.sh
+++ b/utils/install-dependencies.sh
@@ -19,6 +19,11 @@
set -ex
+_APISIX_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# shellcheck source=utils/install-rust-toolchain.sh
+. "${_APISIX_UTILS_DIR}/install-rust-toolchain.sh"
+unset _APISIX_UTILS_DIR
+
function detect_aur_helper() {
if [[ $(command -v yay) ]]; then
AUR_HELPER=yay
@@ -33,7 +38,8 @@ function detect_aur_helper() {
function install_dependencies_with_aur() {
detect_aur_helper
$AUR_HELPER -S openresty --noconfirm
- sudo pacman -S openssl --noconfirm
+ sudo pacman -S openssl base-devel git --noconfirm
+ install_rust_toolchain
export OPENRESTY_PREFIX=/opt/openresty
@@ -57,6 +63,7 @@ function install_dependencies_with_yum() {
gcc gcc-c++ curl wget unzip xz gnupg perl-ExtUtils-Embed cpanminus
patch libyaml-devel \
perl perl-devel pcre pcre-devel pcre2 pcre2-devel openldap-devel \
openresty-zlib-devel openresty-pcre-devel
+ install_rust_toolchain
}
# Install dependencies on ubuntu and debian
@@ -79,6 +86,7 @@ function install_dependencies_with_apt() {
# install some compilation tools
sudo apt-get install -y curl make gcc g++ cpanminus libpcre3 libpcre3-dev
libpcre2-dev libyaml-dev unzip openresty-zlib-dev openresty-pcre-dev
+ install_rust_toolchain
}
# Identify the different distributions and call the corresponding function
diff --git a/utils/install-rust-toolchain.sh b/utils/install-rust-toolchain.sh
new file mode 100755
index 000000000..a2351d2cf
--- /dev/null
+++ b/utils/install-rust-toolchain.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+function rustc_meets_minimum_version() {
+ if ! command -v rustc >/dev/null 2>&1; then
+ return 1
+ fi
+
+ local version major minor
+ version=$(rustc --version | awk '{print $2}')
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+ ]]; then
+ return 1
+ fi
+
+ major=${version%%.*}
+ minor=${version#*.}
+ minor=${minor%%.*}
+
+ [ "$major" -gt 1 ] || { [ "$major" -eq 1 ] && [ "$minor" -ge 77 ]; }
+}
+
+function run_as_root() {
+ if [ "$(id -u)" -eq 0 ]; then
+ "$@"
+ else
+ sudo "$@"
+ fi
+}
+
+function install_rustup_toolchain() {
+ local version target checksum tmp_dir tmp rustup_home cargo_home base_path
+ version="1.28.2"
+ rustup_home="${RUSTUP_HOME:-/usr/local/rustup}"
+ cargo_home="${CARGO_HOME:-/usr/local/cargo}"
+ base_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+ case "$(uname -m)" in
+ x86_64|amd64)
+ target="x86_64-unknown-linux-gnu"
+
checksum="20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c"
+ ;;
+ aarch64|arm64)
+ target="aarch64-unknown-linux-gnu"
+
checksum="e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c"
+ ;;
+ *)
+ echo "unsupported architecture for rustup-init: $(uname -m)"
+ exit 1
+ ;;
+ esac
+
+ tmp_dir=$(mktemp -d) || return 1
+ (
+ set -euo pipefail
+ trap 'rm -rf "${tmp_dir}"' EXIT
+ tmp="${tmp_dir}/rustup-init"
+ curl -fsSLo "$tmp"
"https://static.rust-lang.org/rustup/archive/${version}/${target}/rustup-init"
+ echo "${checksum} ${tmp}" | sha256sum -c -
+ chmod +x "$tmp"
+ run_as_root mkdir -p "$rustup_home" "$cargo_home"
+ run_as_root env RUSTUP_HOME="$rustup_home" CARGO_HOME="$cargo_home"
PATH="$base_path" \
+ "$tmp" -y --profile minimal --default-toolchain stable
--no-modify-path
+ run_as_root env RUSTUP_HOME="$rustup_home" CARGO_HOME="$cargo_home"
PATH="${cargo_home}/bin:${base_path}" \
+ "${cargo_home}/bin/rustup" default stable
+ ) || return 1
+ export RUSTUP_HOME="$rustup_home"
+ export CARGO_HOME="$cargo_home"
+ export PATH="${cargo_home}/bin:${PATH}"
+ if [[ -n "${GITHUB_ENV:-}" ]]; then
+ {
+ echo "RUSTUP_HOME=${rustup_home}"
+ echo "CARGO_HOME=${cargo_home}"
+ } >> "$GITHUB_ENV"
+ fi
+ if [[ -n "${GITHUB_PATH:-}" ]]; then
+ echo "${cargo_home}/bin" >> "$GITHUB_PATH"
+ fi
+ run_as_root ln -sf "${cargo_home}/bin/cargo" /usr/local/bin/cargo
+ run_as_root ln -sf "${cargo_home}/bin/rustc" /usr/local/bin/rustc
+}
+
+function install_rust_toolchain() {
+ if rustc_meets_minimum_version; then
+ return
+ fi
+
+ if command -v brew >/dev/null 2>&1; then
+ brew install rust
+ if rustc_meets_minimum_version; then
+ return
+ fi
+ echo "installed rustc is older than 1.77"
+ exit 1
+ fi
+
+ if command -v curl >/dev/null 2>&1 && command -v sha256sum >/dev/null
2>&1; then
+ install_rustup_toolchain
+ if rustc_meets_minimum_version; then
+ return
+ fi
+ echo "installed rustc is older than 1.77"
+ exit 1
+ fi
+
+ if command -v apt-get >/dev/null 2>&1; then
+ run_as_root apt-get install -y cargo
+ if rustc_meets_minimum_version; then
+ return
+ fi
+ echo "installed rustc is older than 1.77"
+ exit 1
+ fi
+
+ if command -v yum >/dev/null 2>&1; then
+ run_as_root yum install -y cargo rust
+ if rustc_meets_minimum_version; then
+ return
+ fi
+ echo "installed rustc is older than 1.77"
+ exit 1
+ fi
+
+ if command -v pacman >/dev/null 2>&1; then
+ run_as_root pacman -S rust --noconfirm
+ if rustc_meets_minimum_version; then
+ return
+ fi
+ echo "installed rustc is older than 1.77"
+ exit 1
+ fi
+
+ echo "No supported Rust package manager found"
+ exit 1
+}