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 5c5e1b2cb5 fix(ai-proxy-multi): keep the client request body intact
across fallback retries (#13793)
5c5e1b2cb5 is described below
commit 5c5e1b2cb54564d817cbd27f18b223678a792a39
Author: Mohammad Izzraff Janius
<[email protected]>
AuthorDate: Wed Aug 12 10:03:35 2026 +0800
fix(ai-proxy-multi): keep the client request body intact across fallback
retries (#13793)
---
apisix/plugins/ai-proxy/base.lua | 6 +--
t/plugin/ai-proxy-multi-retry.t | 90 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 92 insertions(+), 4 deletions(-)
diff --git a/apisix/plugins/ai-proxy/base.lua b/apisix/plugins/ai-proxy/base.lua
index fa815f430f..5c9e9a5b31 100644
--- a/apisix/plugins/ai-proxy/base.lua
+++ b/apisix/plugins/ai-proxy/base.lua
@@ -187,6 +187,8 @@ function _M.before_proxy(conf, ctx, on_error)
return 400, err
end
+ request_body = core.table.deepcopy(request_body)
+
local extra_opts = {
name = ai_instance.name,
endpoint = ai_instance._resolved_endpoint
@@ -242,10 +244,6 @@ function _M.before_proxy(conf, ctx, on_error)
-- Step 2: Extract model from request
local request_model = request_body.model
-
- if request_model then
- ctx.var.request_llm_model = request_model
- end
local model = ai_instance.options and ai_instance.options.model or
request_model
if model then
ctx.var.llm_model = model
diff --git a/t/plugin/ai-proxy-multi-retry.t b/t/plugin/ai-proxy-multi-retry.t
index e09bfc3400..a5f8d7cb2f 100644
--- a/t/plugin/ai-proxy-multi-retry.t
+++ b/t/plugin/ai-proxy-multi-retry.t
@@ -34,6 +34,7 @@ add_block_preprocessor(sub {
plugins:
- ai-proxy-multi
- prometheus
+ - serverless-post-function
_EOC_
$block->set_value("extra_yaml_config", $user_yaml_config);
@@ -75,6 +76,33 @@ _EOC_
}
}
}
+ # Upstream that echoes the request body it receives inside a
well-formed
+ # chat completion so the test can assert exactly what was forwarded to
+ # the fallback instance.
+ server {
+ server_name echo_instance;
+ default_type 'application/json';
+ listen 6734;
+ location / {
+ content_by_lua_block {
+ local json = require("cjson.safe")
+ ngx.req.read_body()
+ local raw = ngx.req.get_body_data() or ""
+ ngx.status = 200
+ ngx.say(json.encode({
+ id = "chatcmpl-echo",
+ object = "chat.completion",
+ model = "echo",
+ choices = {{
+ index = 0,
+ message = { role = "assistant", content = raw },
+ finish_reason = "stop",
+ }},
+ usage = { prompt_tokens = 1, completion_tokens = 1,
total_tokens = 2 },
+ }))
+ }
+ }
+ }
_EOC_
$block->set_value("http_config", $http_config);
@@ -217,3 +245,65 @@ POST /anything
--- response_body_like: slow internal error
--- error_log
exceeding retry_on_failure_within_ms 200
+
+
+
+=== TEST 7: fallback preserves the client body: set up asymmetric instances
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/anything",
+ "plugins": {
+ "ai-proxy-multi": {
+ "fallback_strategy": ["http_5xx"],
+ "instances": [
+
{"name":"err-a","provider":"openai-compatible","weight":1,"priority":10,"auth":{"header":{"Authorization":"Bearer
token"}},"options":{"model":"upstream-model-A","temperature":0.9},"override":{"endpoint":"http://127.0.0.1:6731"}},
+
{"name":"echo-b","provider":"openai-compatible","weight":1,"priority":0,"auth":{"header":{"Authorization":"Bearer
token"}},"options":{"model":"upstream-model-B"},"override":{"endpoint":"http://127.0.0.1:6734"}}
+ ],
+ "ssl_verify": false
+ },
+ "serverless-post-function": {
+ "phase": "log",
+ "functions": ["return function(conf, ctx)
ngx.log(ngx.WARN, \"FALLBACKVARS request_llm_model=\",
tostring(ctx.var.request_llm_model), \" llm_model=\",
tostring(ctx.var.llm_model)) end"]
+ }
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 8: retry keeps the original model in vars and does not leak instance
A options into instance B
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http").new()
+ local cjson = require("cjson.safe")
+ local res = assert(http:request_uri(
+ "http://127.0.0.1:" .. ngx.var.server_port .. "/anything", {
+ method = "POST",
+ body = '{ "model": "client-model", "messages": [ { "role":
"user", "content": "What is 1+1?"} ] }',
+ headers = { ["Content-Type"] = "application/json" },
+ }))
+ local completion = cjson.decode(res.body)
+ local forwarded =
cjson.decode(completion.choices[1].message.content)
+ ngx.say("model=", forwarded.model)
+ ngx.say(forwarded.temperature == nil and "no temperature leak" or
"temperature leaked")
+ }
+ }
+--- response_body
+model=upstream-model-B
+no temperature leak
+--- error_log
+FALLBACKVARS request_llm_model=client-model llm_model=upstream-model-B