This is an automated email from the ASF dual-hosted git repository.

shreemaan-abhishek 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 f0bb251a9 fix(ai-cache): key the passthrough protocol on the client 
method, path and query (#13887)
f0bb251a9 is described below

commit f0bb251a97652bb655b03e39e81fda1a3e1e93b2
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Sep 1 18:56:02 2026 +0800

    fix(ai-cache): key the passthrough protocol on the client method, path and 
query (#13887)
---
 apisix/plugins/ai-cache/key.lua    |  22 +++-
 docs/en/latest/plugins/ai-cache.md |   2 +
 docs/zh/latest/plugins/ai-cache.md |   2 +
 t/plugin/ai-cache.t                | 214 +++++++++++++++++++++++++++++++++++++
 4 files changed, 235 insertions(+), 5 deletions(-)

diff --git a/apisix/plugins/ai-cache/key.lua b/apisix/plugins/ai-cache/key.lua
index 9c6542d28..1d1594388 100644
--- a/apisix/plugins/ai-cache/key.lua
+++ b/apisix/plugins/ai-cache/key.lua
@@ -69,12 +69,24 @@ local function build_repr(ctx, body, messages)
     local proto = ctx.ai_client_protocol and 
protocols.get(ctx.ai_client_protocol)
     params.stream = (proto and proto.is_streaming(body)) == true
 
+    local client = {
+        protocol = ctx.ai_client_protocol or "",
+        messages = messages,
+        params   = params,
+    }
+    -- passthrough proxies the client's method, path and query string verbatim
+    -- (see ai-proxy/base.lua), so under it they select the upstream endpoint
+    -- and are response-determining.
+    if ctx.ai_client_protocol == "passthrough" then
+        -- live method: ctx.var.request_method is cached at route match, before
+        -- a proxy-rewrite method change that the upstream request carries.
+        client.method = core.request.get_method()
+        client.uri    = ctx.var.uri
+        client.args   = ctx.var.args
+    end
+
     return {
-        client = {
-            protocol = ctx.ai_client_protocol or "",
-            messages = messages,
-            params   = params,
-        },
+        client = client,
         effective = {
             provider    = inst.provider,
             -- effective model precedence mirrors ai-proxy/base.lua exactly:
diff --git a/docs/en/latest/plugins/ai-cache.md 
b/docs/en/latest/plugins/ai-cache.md
index 907b8d177..3443f86b8 100644
--- a/docs/en/latest/plugins/ai-cache.md
+++ b/docs/en/latest/plugins/ai-cache.md
@@ -61,6 +61,8 @@ By default the cache is isolated per route, so two routes 
never serve each other
 
 Even with `cache_key.share_across_routes` enabled, the cache key identifies 
the *effective* upstream request — the request `ai-proxy` actually sends after 
applying the AI instance's `provider`, `options` (model, temperature, and other 
model parameters) and `override`. Routes that would call the model differently 
therefore keep separate cache entries, so one route's response is never served 
for another.
 
+For the `passthrough` protocol, `ai-proxy` forwards the client's request 
method, path and query string verbatim, so the key includes them too: the same 
body sent to two different upstream paths, or with different query parameters, 
keeps separate entries.
+
 :::
 
 ## Attributes
diff --git a/docs/zh/latest/plugins/ai-cache.md 
b/docs/zh/latest/plugins/ai-cache.md
index 5c9e51234..db4d50fc2 100644
--- a/docs/zh/latest/plugins/ai-cache.md
+++ b/docs/zh/latest/plugins/ai-cache.md
@@ -61,6 +61,8 @@ import TabItem from '@theme/TabItem';
 
 即使开启 `cache_key.share_across_routes`,来自不同上游模型或 provider 
的响应也会分别存储在各自的缓存条目中,因此某个模型的响应绝不会被返回给另一个模型。
 
+对于 `passthrough` 协议,`ai-proxy` 
会原样转发客户端的请求方法、路径和查询字符串,因此缓存键也会包含它们:相同的请求体发送到两个不同的上游路径,或携带不同的查询参数时,会分别保存为独立的缓存条目。
+
 :::
 
 ## 属性
diff --git a/t/plugin/ai-cache.t b/t/plugin/ai-cache.t
index 5419d5ef9..d87c2d3a7 100644
--- a/t/plugin/ai-cache.t
+++ b/t/plugin/ai-cache.t
@@ -1446,3 +1446,217 @@ X-AI-Cache-Status: MISS
     }
 --- response_body
 passed
+
+
+
+=== TEST 58: passthrough folds the client method, path and query into the 
fingerprint (key.lua unit)
+--- config
+    location /t {
+        content_by_lua_block {
+            local key = require("apisix.plugins.ai-cache.key")
+
+            local inst = { provider = "openai", options = {}, override = {} }
+            local body = { model = "dall-e-3", prompt = "otter" }
+
+            local function fp(protocol, var)
+                local ctx = { ai_client_protocol = protocol, var = var,
+                              picked_ai_instance = inst }
+                return key.fingerprint(ctx, body)
+            end
+
+            local base = { uri = "/v1/images/generations", args = nil }
+            local pt   = fp("passthrough", base)
+
+            assert(fp("passthrough", base) == pt, "identical passthrough 
request must match")
+            assert(fp("passthrough", { uri = "/v1/images/edits" }) ~= pt, 
"different path")
+            assert(fp("passthrough", { uri = "/v1/images/generations",
+                                       args = "api-version=2024-02-01" }) ~= 
pt,
+                   "different query string")
+
+            -- the method is read live from the request (a rewrite changes 
it), so
+            -- flip it the way proxy-rewrite does rather than through ctx.var
+            ngx.req.set_method(ngx.HTTP_POST)
+            local posted = fp("passthrough", base)
+            assert(posted ~= pt, "different method")
+            ngx.req.set_method(ngx.HTTP_GET)
+            assert(fp("passthrough", base) == pt, "method restored")
+
+            -- other protocols build a fixed upstream request from the body 
alone,
+            -- so the client path stays out of their fingerprint
+            local chat_ctx = { ai_client_protocol = "openai-chat", var = { uri 
= "/a" },
+                               picked_ai_instance = inst }
+            local chat_body = { model = "gpt-4o", messages = {{ role = "user", 
content = "hi" }} }
+            local chat_fp = key.fingerprint(chat_ctx, chat_body)
+            chat_ctx.var = { uri = "/b" }
+            assert(key.fingerprint(chat_ctx, chat_body) == chat_fp,
+                   "non-passthrough fingerprint ignores the client path")
+
+            ngx.say("passed")
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 59: wildcard passthrough route: ai-proxy without an endpoint path 
forwards the client URI
+--- config
+    location /t {
+        content_by_lua_block {
+            require("lib.test_redis").flush_port("127.0.0.1", 6379)
+
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/v1/*",
+                    "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
test-key" } },
+                            "override": { "endpoint": "http://127.0.0.1:1980"; }
+                        },
+                        "ai-cache": {
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6379
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 60: passthrough body to /v1/images/generations is a MISS and is 
proxied
+--- request
+POST /v1/images/generations
+{"model":"dall-e-3","prompt":"ai-cache passthrough same body"}
+--- more_headers
+X-AI-Fixture: openai/images-generation.json
+--- response_headers
+X-AI-Cache-Status: MISS
+--- response_body_like eval
+qr/baby sea otter/
+--- wait: 0.3
+
+
+
+=== TEST 61: the SAME body to a different upstream path is a MISS, not the 
cached images response
+--- request
+POST /v1/chat/completions
+{"model":"dall-e-3","prompt":"ai-cache passthrough same body"}
+--- more_headers
+X-AI-Fixture: openai/chat-basic.json
+--- response_headers
+X-AI-Cache-Status: MISS
+--- response_body_like eval
+qr/1 \+ 1 = 2/
+--- wait: 0.3
+
+
+
+=== TEST 62: the SAME body and path with a different query string is a MISS
+--- request
+POST /v1/images/generations?api-version=2024-02-01
+{"model":"dall-e-3","prompt":"ai-cache passthrough same body"}
+--- more_headers
+X-AI-Fixture: openai/images-generation.json
+--- response_headers
+X-AI-Cache-Status: MISS
+--- response_body_like eval
+qr/baby sea otter/
+--- wait: 0.3
+
+
+
+=== TEST 63: repeating the first passthrough request exactly is a HIT
+--- request
+POST /v1/images/generations
+{"model":"dall-e-3","prompt":"ai-cache passthrough same body"}
+--- response_headers_like
+X-AI-Cache-Status: HIT
+--- response_body_like eval
+qr/baby sea otter/
+
+
+
+=== TEST 64: passthrough route with proxy-rewrite forcing the upstream method 
to POST
+--- config
+    location /t {
+        content_by_lua_block {
+            require("lib.test_redis").flush_port("127.0.0.1", 6379)
+
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/v1/*",
+                    "plugins": {
+                        "proxy-rewrite": { "method": "POST" },
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
test-key" } },
+                            "override": { "endpoint": "http://127.0.0.1:1980"; }
+                        },
+                        "ai-cache": {
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6379
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- extra_yaml_config
+plugins:
+  - proxy-rewrite
+  - ai-proxy
+  - ai-cache
+--- response_body
+passed
+
+
+
+=== TEST 65: POST passthrough request is a MISS
+--- request
+POST /v1/images/generations
+{"model":"dall-e-3","prompt":"ai-cache passthrough rewritten method"}
+--- more_headers
+X-AI-Fixture: openai/images-generation.json
+--- extra_yaml_config
+plugins:
+  - proxy-rewrite
+  - ai-proxy
+  - ai-cache
+--- response_headers
+X-AI-Cache-Status: MISS
+--- response_body_like eval
+qr/baby sea otter/
+--- wait: 0.3
+
+
+
+=== TEST 66: PUT with the same body is rewritten to POST upstream, so it is a 
HIT on the POST entry
+--- request
+PUT /v1/images/generations
+{"model":"dall-e-3","prompt":"ai-cache passthrough rewritten method"}
+--- extra_yaml_config
+plugins:
+  - proxy-rewrite
+  - ai-proxy
+  - ai-cache
+--- response_headers_like
+X-AI-Cache-Status: HIT
+--- response_body_like eval
+qr/baby sea otter/

Reply via email to