@@ -20,16 +20,21 @@ local schema = require("apisix.plugins.ai-cache.schema")
2020local key_mod = require (" apisix.plugins.ai-cache.key" )
2121local binding = require (" apisix.plugins.ai-protocols.binding" )
2222local redis_util = require (" apisix.utils.redis" )
23+ local semantic = require (" apisix.plugins.ai-cache.semantic" )
2324
2425local ngx = ngx
2526local ngx_null = ngx .null
2627local ipairs = ipairs
28+ local pcall = pcall
2729local concat = table.concat
30+ local str_format = string.format
2831
29- local CACHE_STATUS_HEADER = " X-AI-Cache-Status"
30- local CACHE_AGE_HEADER = " X-AI-Cache-Age"
31- local DEFAULT_TTL = 3600
32- local DEFAULT_MAX_BODY = 1048576
32+ local CACHE_STATUS_HEADER = " X-AI-Cache-Status"
33+ local CACHE_AGE_HEADER = " X-AI-Cache-Age"
34+ local CACHE_SIMILARITY_HEADER = " X-AI-Cache-Similarity"
35+ local DEFAULT_TTL = 3600
36+ local DEFAULT_MAX_BODY = 1048576
37+ local DEFAULT_SEMANTIC_TTL = 86400
3338
3439local _M = {
3540 version = 0.1 ,
@@ -39,8 +44,31 @@ local _M = {
3944}
4045
4146
47+ local function has_layer (conf , name )
48+ local layers = conf .layers or { " exact" }
49+ for _ , l in ipairs (layers ) do
50+ if l == name then
51+ return true
52+ end
53+ end
54+ return false
55+ end
56+
57+
4258function _M .check_schema (conf )
43- return core .schema .check (schema , conf )
59+ core .utils .check_https ({
60+ " semantic.embedding.openai.endpoint" ,
61+ " semantic.embedding.azure_openai.endpoint" ,
62+ }, conf , _M .name )
63+ local ok , err = core .schema .check (schema , conf )
64+ if not ok then
65+ return false , err
66+ end
67+ if conf .semantic and not has_layer (conf , " semantic" ) then
68+ core .log .warn (" ai-cache: 'semantic' is configured but not listed in " ,
69+ " 'layers'; the semantic (L2) cache is inactive" )
70+ end
71+ return true
4472end
4573
4674
@@ -53,12 +81,17 @@ local function release(conf, red)
5381end
5482
5583
56- local function serve_hit (conf , ctx , cached )
57- ctx .ai_cache_status = " HIT"
84+ local function serve_hit (conf , ctx , cached , similarity )
85+ local status = " HIT"
86+ ctx .ai_cache_status = status
5887 if conf .cache_headers ~= false then
59- core .response .set_header (CACHE_STATUS_HEADER , " HIT " )
88+ core .response .set_header (CACHE_STATUS_HEADER , status )
6089 local age = ngx .time () - (cached .created_at or ngx .time ())
6190 core .response .set_header (CACHE_AGE_HEADER , age < 0 and 0 or age )
91+ if similarity then
92+ core .response .set_header (CACHE_SIMILARITY_HEADER ,
93+ str_format (" %.4f" , similarity ))
94+ end
6295 end
6396 core .response .set_header (" Content-Type" , " application/json" )
6497 return core .response .exit (200 , cached .body )
@@ -126,16 +159,56 @@ function _M.access(conf, ctx)
126159 ctx .ai_cache_status = " MISS"
127160 return
128161 end
129- release (conf , red )
130-
131162 if res ~= nil and res ~= ngx_null then
132163 local cached = core .json .decode (res )
133164 if cached and cached .body then
165+ release (conf , red )
134166 return serve_hit (conf , ctx , cached )
135167 end
136168 core .log .warn (" ai-cache: discarding malformed cache entry for " , ctx .ai_cache_key )
137169 end
138170
171+ -- L1 miss -> L2 semantic lookup. Release the L1 connection before
172+ -- embed_query()'s HTTP call so the pool isn't pinned across the embedding
173+ -- round-trip; re-acquire for the vector search. pcall keeps throws fail-open.
174+ if has_layer (conf , " semantic" ) and conf .semantic then
175+ release (conf , red )
176+
177+ local ok , vec = pcall (semantic .embed_query , conf , ctx , body )
178+ if not ok then
179+ core .log .warn (" ai-cache: semantic embed error, fail-open as MISS: " , vec )
180+ vec = nil
181+ -- prevent log() from scheduling a write with partial/bad state
182+ ctx .ai_cache_embedding = nil
183+ end
184+
185+ if vec then
186+ local sred
187+ sred , err = redis_util .new (conf )
188+ if not sred then
189+ core .log .warn (" ai-cache: redis unavailable for semantic search, " ,
190+ " fail-open as MISS: " , err )
191+ else
192+ local sok , hit = pcall (semantic .search , sred , conf , ctx , vec )
193+ if not sok then
194+ sred :close ()
195+ core .log .warn (" ai-cache: semantic search error, fail-open as MISS: " , hit )
196+ ctx .ai_cache_embedding = nil
197+ else
198+ release (conf , sred )
199+ if hit then
200+ return serve_hit (conf , ctx ,
201+ { body = hit .body , created_at = hit .created_at }, hit .similarity )
202+ end
203+ end
204+ end
205+ end
206+
207+ ctx .ai_cache_status = " MISS"
208+ return
209+ end
210+
211+ release (conf , red )
139212 ctx .ai_cache_status = " MISS"
140213end
141214
174247-- The response-capturing phases (body_filter / log) run in contexts where
175248-- cosockets are disabled, so the Redis write is deferred to a 0-delay timer
176249-- (timers run in a light thread where cosockets are allowed).
177- local function write_to_cache (premature , conf , cache_key , response_body )
250+ -- l2 (optional) = { partition, embedding, dim, fingerprint, ttl } for L2 write.
251+ local function write_to_cache (premature , conf , cache_key , response_body , l2 )
178252 if premature then
179253 return
180254 end
@@ -192,6 +266,13 @@ local function write_to_cache(premature, conf, cache_key, response_body)
192266 core .log .warn (" ai-cache: redis set failed: " , err )
193267 return
194268 end
269+ if l2 then
270+ l2 .created_at = ngx .time ()
271+ local wok , werr = pcall (semantic .write , red , conf , l2 , response_body )
272+ if not wok then
273+ core .log .warn (" ai-cache: semantic write error: " , werr )
274+ end
275+ end
195276 release (conf , red )
196277end
197278
@@ -218,7 +299,21 @@ function _M.log(conf, ctx)
218299 local response_body = concat (buf , " " , 1 , buf .n )
219300
220301 local cache_key = key_mod .build (conf , ctx , ctx .ai_cache_fingerprint )
221- local ok , err = ngx .timer .at (0 , write_to_cache , conf , cache_key , response_body )
302+
303+ -- Build the L2 doc from ctx fields stashed by semantic.embed_query(); the
304+ -- embedding is only set on a successful embed, so a nil check guards the write.
305+ local l2
306+ if has_layer (conf , " semantic" ) and ctx .ai_cache_embedding then
307+ l2 = {
308+ partition = ctx .ai_cache_partition ,
309+ embedding = ctx .ai_cache_embedding ,
310+ dim = ctx .ai_cache_dim ,
311+ fingerprint = ctx .ai_cache_fingerprint ,
312+ ttl = (conf .semantic and conf .semantic .ttl ) or DEFAULT_SEMANTIC_TTL ,
313+ }
314+ end
315+
316+ local ok , err = ngx .timer .at (0 , write_to_cache , conf , cache_key , response_body , l2 )
222317 if not ok then
223318 core .log .warn (" ai-cache: failed to schedule cache write: " , err )
224319 end
0 commit comments