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
+}


Reply via email to