Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
af244a4
feat(ai): support configurable request JSON library
nic-6443 May 19, 2026
4f0fe94
chore: satisfy request json lint
nic-6443 May 19, 2026
ff40b98
fix: harden request JSON selector
nic-6443 May 19, 2026
4ed03d7
build: install simdjson dependency from source
nic-6443 May 19, 2026
f0f1614
docs: add JSON library benchmark data
nic-6443 May 19, 2026
162460d
docs: remove private benchmark source link
nic-6443 May 19, 2026
d022938
fix: address JSON library dependency review
nic-6443 May 19, 2026
a16f402
ci: ensure recent Rust for qjson builds
nic-6443 May 19, 2026
5c6a9c4
ci: run rustup-init with expected name
nic-6443 May 19, 2026
024cba3
test: allow parser-specific JSON errors
nic-6443 May 19, 2026
854c919
ci: harden JSON dependency installers
nic-6443 May 19, 2026
71a8775
docs: note simdjson round-trip limits
nic-6443 May 19, 2026
985a1d3
ci: handle rustup symlink privileges
nic-6443 May 19, 2026
8ab89aa
fix: materialize qjson request body decode
nic-6443 May 19, 2026
2b7d77c
chore: tighten rustup and simdjson install helpers
nic-6443 May 19, 2026
8ee86b0
chore: rely on schema for JSON lib config
nic-6443 May 19, 2026
e4bad9e
chore: simplify request JSON dispatch
nic-6443 May 19, 2026
915d46b
docs: explain simdjson encode path
nic-6443 May 19, 2026
c3fbda1
ci: install simdjson for LuaRocks smoke
nic-6443 May 19, 2026
f97c6a4
chore: share Rust toolchain installer
nic-6443 May 19, 2026
802bbe0
chore: share Rust toolchain setup
nic-6443 May 19, 2026
9cc97e1
ci: install simdjson for checkout smoke
nic-6443 May 19, 2026
c9443ba
test: make nacos fallback host deterministic
nic-6443 May 19, 2026
3ddcc21
test: share nacos fallback marker across workers
nic-6443 May 19, 2026
129d575
feat(ai): default request JSON library to simdjson
nic-6443 May 19, 2026
78c02c7
ci: harden Rust toolchain fallback
nic-6443 May 19, 2026
47f3537
ci: persist Rust toolchain selection
nic-6443 May 19, 2026
b960cf0
ci: export Rust toolchain environment
nic-6443 May 19, 2026
36a9043
ci: prefer pinned rustup toolchain on Linux
nic-6443 May 19, 2026
b4f4edb
ci: expose Rust environment in Docker builds
nic-6443 May 19, 2026
2e8e607
ci: pin rustup-init version with checksum
nic-6443 May 19, 2026
cd53052
ci: install Arch build tools for simdjson
nic-6443 May 19, 2026
0670af4
ci: preserve Rust env for dependency builds
nic-6443 May 19, 2026
fa86179
ci: pass Rust env to LuaRocks installs
nic-6443 May 19, 2026
db52531
ci: report outdated Rust packages clearly
nic-6443 May 19, 2026
3a30b9f
test: revert unrelated nacos change
nic-6443 May 19, 2026
8219798
fix: use api7 simdjson package
nic-6443 May 20, 2026
3d70dcb
test: make simdjson array check explicit
nic-6443 May 20, 2026
2604778
build: install simdjson through rockspec
nic-6443 May 20, 2026
1b1d302
build: use simdjson 0.1.1
nic-6443 May 20, 2026
884f954
build: choose writable Rust home for LuaRocks deps
nic-6443 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ deps: install-runtime
$(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); \
./utils/install-lua-resty-simdjson.sh deps; \
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; \
Expand Down
1 change: 1 addition & 0 deletions apisix-master-0.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ 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",
}

build = {
Expand Down
1 change: 1 addition & 0 deletions apisix/cli/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ local _M = {
enable_server_tokens = true,
extra_lua_path = "",
extra_lua_cpath = "",
request_body_json_lib = "qjson",
Comment thread
nic-6443 marked this conversation as resolved.
Outdated
Comment thread
nic-6443 marked this conversation as resolved.
Outdated
proxy_cache = {
cache_ttl = "10s",
zones = {
Expand Down
4 changes: 4 additions & 0 deletions apisix/cli/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ local config_schema = {
},
}
},
request_body_json_lib = {
enum = {"cjson", "simdjson", "qjson"},
default = "qjson",
},
proxy_cache = {
type = "object",
properties = {
Expand Down
4 changes: 2 additions & 2 deletions apisix/core/request.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
172 changes: 172 additions & 0 deletions apisix/core/request_json.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
--
-- 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 log = require("apisix.core.log")
local core_json = require("apisix.core.json")
local require = require
local pcall = pcall


local DEFAULT_JSON_LIB = "qjson"
local json_libs = {
cjson = true,
qjson = true,
simdjson = true,
}

local qjson
local simdjson_parser
local qjson_unavailable
local simdjson_unavailable
local configured_name
local warned_invalid_json_lib
local warned_load_failure = {}

local _M = {}


local function configured_json_lib()
if configured_name then
return configured_name
end

local local_conf = config_local.local_conf()
local name = local_conf and local_conf.apisix
and local_conf.apisix.request_body_json_lib
or DEFAULT_JSON_LIB

if not json_libs[name] then
if not warned_invalid_json_lib then
warned_invalid_json_lib = true
log.warn("invalid apisix.request_body_json_lib: ", name,
", fallback to ", DEFAULT_JSON_LIB)
end
name = DEFAULT_JSON_LIB
end

configured_name = name
return configured_name
end


local function normalize_result(ok, res, err)
if not ok then
return nil, res
end

return res, err
end


local function qjson_module()
if qjson then
return qjson
end

if qjson_unavailable then
return nil, qjson_unavailable
end

local ok, mod = pcall(require, "qjson")
if not ok then
qjson_unavailable = "failed to load qjson: " .. mod
return nil, qjson_unavailable
end

qjson = mod
return qjson
end


local function warn_load_failure(name, err)
if warned_load_failure[name] then
return
end

warned_load_failure[name] = true
log.warn(err, ", fallback to cjson")
end


local function simdjson_decode(str)
if simdjson_unavailable then
return nil, simdjson_unavailable, true
end

if not simdjson_parser then
local ok, simdjson = pcall(require, "resty.simdjson")
if not ok then
simdjson_unavailable = "failed to load simdjson: " .. simdjson
return nil, simdjson_unavailable, true
end

local parser, err = simdjson.new()
if not parser then
simdjson_unavailable = "failed to create simdjson parser: "
.. (err or "unknown")
return nil, simdjson_unavailable, true
end
simdjson_parser = parser
end

return normalize_result(pcall(simdjson_parser.decode, simdjson_parser, str))
end


function _M.decode(str)
local name = configured_json_lib()
if name == "cjson" then
return core_json.decode(str)
end

if name == "simdjson" then
local res, err, can_fallback = simdjson_decode(str)
if can_fallback then
warn_load_failure(name, err)
return core_json.decode(str)
end

return res, err
end

local mod, err = qjson_module()
if not mod then
warn_load_failure(name, err)
return core_json.decode(str)
end

return normalize_result(pcall(mod.decode, str))
end


function _M.encode(data)
if configured_json_lib() == "qjson" then
local mod, err = qjson_module()
if not mod then
warn_load_failure("qjson", err)
return normalize_result(pcall(core_json.encode, data))
end

return normalize_result(pcall(mod.encode, data))
end

return normalize_result(pcall(core_json.encode, data))
end


return _M
4 changes: 2 additions & 2 deletions apisix/plugins/ai-transport/auth-aws.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apisix/plugins/ai-transport/http.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
-- Provides HTTP client lifecycle management for AI provider requests.

local core = require("apisix.core")
Comment thread
nic-6443 marked this conversation as resolved.
local request_json = require("apisix.core.request_json")
local http = require("resty.http")
local ngx_now = ngx.now
local pairs = pairs
Expand Down Expand Up @@ -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"), {
Expand Down
51 changes: 50 additions & 1 deletion ci/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,64 @@ set_coredns() {

GRPC_SERVER_EXAMPLE_VER=20210819

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}')
major=${version%%.*}
minor=${version#*.}
minor=${minor%%.*}

[ "$major" -gt 1 ] || { [ "$major" -eq 1 ] && [ "$minor" -ge 77 ]; }
}

install_recent_rust_toolchain () {
if rustc_meets_minimum_version; then
return
fi

local version target checksum tmp_dir tmp
version="${RUSTUP_INIT_VERSION:-1.28.2}"
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)
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"
"$tmp" -y --profile minimal --default-toolchain stable
export PATH="${HOME}/.cargo/bin:${PATH}"
ln -sf "${HOME}/.cargo/bin/cargo" /usr/local/bin/cargo
ln -sf "${HOME}/.cargo/bin/rustc" /usr/local/bin/rustc
}

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_recent_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() {
Expand Down
3 changes: 3 additions & 0 deletions conf/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ 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: qjson # JSON library used to decode request bodies parsed by core.request.
# Also controls AI upstream request body encoding.
# Can be cjson, simdjson, or qjson.
# lua_module_hook: "my_project.my_hook" # Hook module used to inject third-party code into APISIX.

proxy_cache: # Proxy Caching configuration
Expand Down
21 changes: 21 additions & 0 deletions docs/en/latest/plugins/ai-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ 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: qjson
```

Valid values are `cjson`, `simdjson`, and `qjson`. The default is `qjson`. When `simdjson` is configured, APISIX uses `simdjson` to decode request bodies and `cjson` to encode AI upstream request bodies.

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 `qjson` for the best throughput on large AI request bodies. Use `simdjson` when you only want to accelerate request body decoding while keeping `cjson` encoding semantics for AI upstream request bodies.

## Request Format

| Name | Type | Required | Description |
Expand Down
21 changes: 21 additions & 0 deletions docs/zh/latest/plugins/ai-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ 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: qjson
```

有效值为 `cjson`、`simdjson` 和 `qjson`,默认值为 `qjson`。当配置为 `simdjson` 时,APISIX 使用 `simdjson` 解码请求体,并使用 `cjson` 编码 AI 上游请求体。

以下性能数据来自使用大 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 请求体场景获得最高吞吐,推荐使用 `qjson`。如果只希望加速请求体解码,同时保持 AI 上游请求体使用 `cjson` 编码语义,可以使用 `simdjson`。

## 请求格式

| 名称 | 类型 | 必选项 | 描述 |
Expand Down
Loading
Loading