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 d0c3ceed1 feat(prometheus): add metrics for AI cache hits, misses, 
bypasses, and embedding latency (#13659)
d0c3ceed1 is described below

commit d0c3ceed12f14b90b43971a5e50406399749a388
Author: Mohammad Izzraff Janius 
<[email protected]>
AuthorDate: Tue Jul 7 10:59:34 2026 +0800

    feat(prometheus): add metrics for AI cache hits, misses, bypasses, and 
embedding latency (#13659)
---
 apisix/plugins/ai-cache.lua            |   1 +
 apisix/plugins/ai-cache/semantic.lua   |   3 +
 apisix/plugins/prometheus.lua          |   1 +
 apisix/plugins/prometheus/exporter.lua | 113 ++++++++++++++
 docs/en/latest/plugins/prometheus.md   |  26 +++-
 docs/zh/latest/plugins/prometheus.md   |  26 +++-
 t/plugin/prometheus-ai-cache.t         | 270 +++++++++++++++++++++++++++++++++
 7 files changed, 438 insertions(+), 2 deletions(-)

diff --git a/apisix/plugins/ai-cache.lua b/apisix/plugins/ai-cache.lua
index 5a09f0d85..62e69436a 100644
--- a/apisix/plugins/ai-cache.lua
+++ b/apisix/plugins/ai-cache.lua
@@ -132,6 +132,7 @@ end
 local function serve_hit(conf, ctx, cached, similarity)
     local status = "HIT"
     ctx.ai_cache_status = status
+    ctx.ai_cache_hit_layer = similarity and "semantic" or "exact"
     if conf.cache_headers ~= false then
         core.response.set_header(CACHE_STATUS_HEADER, status)
         local age = ngx.time() - (cached.created_at or ngx.time())
diff --git a/apisix/plugins/ai-cache/semantic.lua 
b/apisix/plugins/ai-cache/semantic.lua
index 6cb580283..7c2e54da2 100644
--- a/apisix/plugins/ai-cache/semantic.lua
+++ b/apisix/plugins/ai-cache/semantic.lua
@@ -26,6 +26,7 @@ local type   = type
 local next   = next
 local concat = table.concat
 local tostring = tostring
+local ngx_now = ngx.now
 
 -- Pre-require both drivers so a misconfigured provider name cannot escape
 -- lookup()'s fail-open boundary via a request-time require() raise.
@@ -262,7 +263,9 @@ function _M.embed_query(conf, ctx, body)
         return nil
     end
 
+    local started = ngx_now()
     local vec, err = embed(conf, text)
+    ctx.ai_cache_embedding_latency = (ngx_now() - started) * 1000
     if not vec then
         core.log.warn("ai-cache: embedding failed, fail-open as MISS: ", err)
         return nil
diff --git a/apisix/plugins/prometheus.lua b/apisix/plugins/prometheus.lua
index b1f3c58b3..fe9e851c7 100644
--- a/apisix/plugins/prometheus.lua
+++ b/apisix/plugins/prometheus.lua
@@ -38,6 +38,7 @@ local structural_labels = {
     http_latency = {type = true},
     bandwidth = {type = true},
     llm_latency = {type = true},
+    ai_cache_hits_total = {layer = true},
 }
 
 
diff --git a/apisix/plugins/prometheus/exporter.lua 
b/apisix/plugins/prometheus/exporter.lua
index de3e4a8bf..ca71a1972 100644
--- a/apisix/plugins/prometheus/exporter.lua
+++ b/apisix/plugins/prometheus/exporter.lua
@@ -158,6 +158,14 @@ local metric_label_map = {
         "request_type", "request_llm_model", "llm_model"},
     llm_completion_tokens_dist = {"route_id", "service_id", "consumer", "node",
         "request_type", "request_llm_model", "llm_model"},
+    ai_cache_hits_total = {"layer", "route", "route_id", "service", 
"service_id",
+        "consumer", "node", "request_type", "request_llm_model", "llm_model"},
+    ai_cache_misses_total = {"route", "route_id", "service", "service_id",
+        "consumer", "node", "request_type", "request_llm_model", "llm_model"},
+    ai_cache_bypasses_total = {"route", "route_id", "service", "service_id",
+        "consumer", "node", "request_type", "request_llm_model", "llm_model"},
+    ai_cache_embedding_latency = {"route", "route_id", "service", "service_id",
+        "consumer", "node", "request_type", "request_llm_model", "llm_model"},
 }
 
 
@@ -282,6 +290,14 @@ function _M.http_init(prometheus_enabled_in_stream)
                                                             
"llm_prompt_tokens_dist", "expire")
     local llm_completion_tokens_dist_exptime = core.table.try_read_attr(attr, 
"metrics",
                                                             
"llm_completion_tokens_dist", "expire")
+    local ai_cache_hits_exptime = core.table.try_read_attr(attr, "metrics",
+                                                            
"ai_cache_hits_total", "expire")
+    local ai_cache_misses_exptime = core.table.try_read_attr(attr, "metrics",
+                                                            
"ai_cache_misses_total", "expire")
+    local ai_cache_bypasses_exptime = core.table.try_read_attr(attr, "metrics",
+                                                            
"ai_cache_bypasses_total", "expire")
+    local ai_cache_embedding_latency_exptime = core.table.try_read_attr(attr, 
"metrics",
+                                                            
"ai_cache_embedding_latency", "expire")
 
     prometheus = base_prometheus.init("prometheus-metrics", metric_prefix)
 
@@ -395,6 +411,35 @@ function _M.http_init(prometheus_enabled_in_stream)
         llm_completion_tokens_buckets,
         llm_completion_tokens_dist_exptime)
 
+    metrics.ai_cache_hits_total = prometheus:counter("ai_cache_hits_total",
+            "Total AI cache hits served, per cache layer",
+            append_tables(metric_label_map.ai_cache_hits_total,
+                          extra_labels("ai_cache_hits_total")),
+            ai_cache_hits_exptime)
+
+    metrics.ai_cache_misses_total = prometheus:counter("ai_cache_misses_total",
+            "Total AI cache misses",
+            append_tables(metric_label_map.ai_cache_misses_total,
+                          extra_labels("ai_cache_misses_total")),
+            ai_cache_misses_exptime)
+
+    metrics.ai_cache_bypasses_total = 
prometheus:counter("ai_cache_bypasses_total",
+            "Total AI cache bypassed requests",
+            append_tables(metric_label_map.ai_cache_bypasses_total,
+                          extra_labels("ai_cache_bypasses_total")),
+            ai_cache_bypasses_exptime)
+
+    local ai_cache_embedding_latency_buckets = DEFAULT_BUCKETS
+    if attr and attr.ai_cache_embedding_latency_buckets then
+        ai_cache_embedding_latency_buckets = 
attr.ai_cache_embedding_latency_buckets
+    end
+    metrics.ai_cache_embedding_latency = 
prometheus:histogram("ai_cache_embedding_latency",
+            "Latency of AI cache embedding calls in milliseconds",
+            append_tables(metric_label_map.ai_cache_embedding_latency,
+                          extra_labels("ai_cache_embedding_latency")),
+            ai_cache_embedding_latency_buckets,
+            ai_cache_embedding_latency_exptime)
+
     if prometheus_enabled_in_stream then
         init_stream_metrics()
     end
@@ -425,6 +470,55 @@ function _M.stream_init()
 end
 
 
+local AI_CACHE_STATUS_METRICS = {
+    HIT    = "ai_cache_hits_total",
+    MISS   = "ai_cache_misses_total",
+    BYPASS = "ai_cache_bypasses_total",
+}
+
+
+-- `layer` is only registered on ai_cache_hits_total, where it leads the label 
list
+local function ai_cache_label_values(name, ctx, layer)
+    local vars = ctx.var
+
+    local route_id = ""
+    local route_name = ""
+    local balancer_ip = ctx.balancer_ip or ""
+    local service_id = ""
+    local service_name = ""
+    local consumer_name = ctx.consumer_name or ""
+
+    local matched_route = ctx.matched_route and ctx.matched_route.value
+    if matched_route then
+        route_id = matched_route.id
+        route_name = matched_route.name or ""
+        service_id = matched_route.service_id or ""
+        if service_id ~= "" then
+            local fetched_service = service_fetch(service_id)
+            service_name = fetched_service and fetched_service.value.name or ""
+        end
+    end
+
+    local disabled_label_metric_map = get_disabled_label_metric_map()
+
+    if layer then
+        return get_enabled_label_values_for_metric(name, 
disabled_label_metric_map,
+            layer, route_name, route_id, service_name, service_id,
+            consumer_name, balancer_ip,
+            vars.request_type, model_to_label(vars.request_llm_model),
+            model_to_label(vars.llm_model),
+            unpack(extra_labels(name, ctx)))
+    end
+
+    return get_enabled_label_values_for_metric(name, disabled_label_metric_map,
+        route_name, route_id, service_name, service_id,
+        consumer_name, balancer_ip,
+        vars.request_type, model_to_label(vars.request_llm_model),
+        model_to_label(vars.llm_model),
+        unpack(extra_labels(name, ctx)))
+end
+
+
 function _M.http_log(conf, ctx)
     local vars = ctx.var
     local disabled_label_metric_map = get_disabled_label_metric_map()
@@ -557,6 +651,24 @@ function _M.http_log(conf, ctx)
                 vars.request_type, request_llm_model_label, llm_model_label,
                 unpack(extra_labels("llm_completion_tokens_dist", ctx))))
     end
+
+    local ai_cache_metric = ctx.ai_cache_status
+                            and AI_CACHE_STATUS_METRICS[ctx.ai_cache_status]
+    if ai_cache_metric then
+        if ctx.ai_cache_status == "HIT" then
+            metrics[ai_cache_metric]:inc(1,
+                ai_cache_label_values(ai_cache_metric, ctx,
+                                      ctx.ai_cache_hit_layer or "exact"))
+        else
+            metrics[ai_cache_metric]:inc(1,
+                ai_cache_label_values(ai_cache_metric, ctx))
+        end
+    end
+
+    if ctx.ai_cache_embedding_latency then
+        
metrics.ai_cache_embedding_latency:observe(ctx.ai_cache_embedding_latency,
+            ai_cache_label_values("ai_cache_embedding_latency", ctx))
+    end
 end
 
 
@@ -974,6 +1086,7 @@ function _M.dec_llm_active_connections(ctx)
     inc_llm_active_connections(ctx, -1)
 end
 
+
 function _M.get_prometheus()
     return prometheus
 end
diff --git a/docs/en/latest/plugins/prometheus.md 
b/docs/en/latest/plugins/prometheus.md
index e31ff269e..45a830209 100644
--- a/docs/en/latest/plugins/prometheus.md
+++ b/docs/en/latest/plugins/prometheus.md
@@ -100,7 +100,7 @@ You can configure the Plugin through its [Plugin 
Metadata](../terminology/plugin
 
 | Name            | Type   | Required | Description                            
                                                                                
                                                                                
                                                                                
    |
 | --------------- | ------ | -------- | 
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
-| disabled_labels | object | False    | Per-metric map of built-in label names 
whose values are collapsed to an empty string `""` to reduce metric 
cardinality. Keyed by metric name: `http_status`, `http_latency`, `bandwidth`, 
`llm_latency`, `llm_prompt_tokens`, `llm_completion_tokens`, 
`llm_active_connections`, `llm_prompt_tokens_dist`, 
`llm_completion_tokens_dist`. Structural labels that define a metric's identity 
(`code` on `http_status`, `type` on `http_latency`, `bandwidth` and `llm_ [...]
+| disabled_labels | object | False    | Per-metric map of built-in label names 
whose values are collapsed to an empty string `""` to reduce metric 
cardinality. Keyed by metric name: `http_status`, `http_latency`, `bandwidth`, 
`llm_latency`, `llm_prompt_tokens`, `llm_completion_tokens`, 
`llm_active_connections`, `llm_prompt_tokens_dist`, 
`llm_completion_tokens_dist`, `ai_cache_hits_total`, `ai_cache_misses_total`, 
`ai_cache_bypasses_total`, `ai_cache_embedding_latency`. Structural labels  
[...]
 
 Collapsing a label's value to `""` keeps the label registered in the metric 
schema, so existing dashboards, `absent()` alerts, and recording rules keep 
working — only the high-cardinality time series that differ solely by those 
labels are collapsed into one. This is useful in dynamic environments such as 
Kubernetes autoscaling, where the upstream node IP (`node` label) churns 
rapidly and would otherwise overflow the `prometheus-metrics` shared dict.
 
@@ -249,6 +249,30 @@ The `type` label distinguishes the kind of latency, 
similar to `apisix_http_late
 | request_type       | traditional_http / ai_chat / ai_stream                  
                                                                        |
 | llm_model       | For non-traditional_http requests, name of the llm_model   
                                                                                
       |
 
+### Labels for the `apisix_ai_cache_*` metrics
+
+The [`ai-cache`](./ai-cache.md) Plugin exports four metrics:
+
+- `apisix_ai_cache_hits_total` counts requests served from the cache, per 
serving layer.
+- `apisix_ai_cache_misses_total` counts requests the Plugin looked up but 
could not serve from the cache.
+- `apisix_ai_cache_bypasses_total` counts requests that bypassed the cache 
lookup entirely.
+- `apisix_ai_cache_embedding_latency` is a histogram of the latency, in 
milliseconds, of the embedding calls made by the semantic layer, measured 
around the embedding provider round-trip for successful and failed calls alike.
+
+They share the following labels:
+
+| Name | Description                                                           
                                                        |
+| ---------- | 
-----------------------------------------------------------------------------------------------------------------------------
 |
+| layer      | Only on `apisix_ai_cache_hits_total`. Cache layer that served 
the hit: `exact` or `semantic`.                                                 
                                |
+| route      | Name of the Route that the metric corresponds to. Default to an 
empty string if the Route has no name or a request does not match any Route.    
                     |
+| route_id      | ID of the Route that the metric corresponds to. Default to 
an empty string if a request does not match any Route.                         |
+| service    | Name of the Service that the matched Route belongs to. Default 
to an empty string if the matched Route does not belong to any Service. |
+| service_id    | ID of the Service that the matched Route belongs to. Default 
to an empty string if the matched Route does not belong to any Service. |
+| consumer   | Name of the Consumer associated with a request. Default to an 
empty string if no Consumer is associated with the request.                     
  |
+| node       | Name of the LLM instance picked by the `ai-proxy` or 
`ai-proxy-multi` Plugin, such as `ai-proxy-openai`. These Plugins report the 
instance name instead of an upstream IP address, on cache hits and misses 
alike.                                                                          
                |
+| request_type       | traditional_http / ai_chat / ai_stream                  
                                                                        |
+| request_llm_model       | Model name requested by the client.                
                                                                          |
+| llm_model       | Model name reported by the LLM response. Empty on cache 
hits, which are served without reaching the LLM.                                
                                                          |
+
 ### Labels for `apisix_http_latency`
 
 The following labels are used to differentiate `apisix_http_latency` metrics.
diff --git a/docs/zh/latest/plugins/prometheus.md 
b/docs/zh/latest/plugins/prometheus.md
index f647b5d6a..054fed1f4 100644
--- a/docs/zh/latest/plugins/prometheus.md
+++ b/docs/zh/latest/plugins/prometheus.md
@@ -100,7 +100,7 @@ plugin_attr:
 
 | 名称            | 类型   | 必选项 | 描述                                              
                                                                                
                                                                                
                                                |
 | --------------- | ------ | ------ | 
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
-| disabled_labels | object | 否     | 按指标配置的内置标签列表,列出的标签其值会被设置为空字符串 `""` 
以降低指标基数。以指标名称作为键:`http_status`、`http_latency`、`bandwidth`、`llm_latency`、`llm_prompt_tokens`、`llm_completion_tokens`、`llm_active_connections`、`llm_prompt_tokens_dist`、`llm_completion_tokens_dist`。定义指标本身含义的结构性标签(`http_status`
 的 `code`、`http_latency`、`bandwidth` 与 `llm_latency` 的 `type`)不可被禁用。 |
+| disabled_labels | object | 否     | 按指标配置的内置标签列表,列出的标签其值会被设置为空字符串 `""` 
以降低指标基数。以指标名称作为键:`http_status`、`http_latency`、`bandwidth`、`llm_latency`、`llm_prompt_tokens`、`llm_completion_tokens`、`llm_active_connections`、`llm_prompt_tokens_dist`、`llm_completion_tokens_dist`、`ai_cache_hits_total`、`ai_cache_misses_total`、`ai_cache_bypasses_total`、`ai_cache_embedding_latency`。定义指标本身含义的结构性标签(`http_status`
 的 `code`、`http_latency`、`bandwidth` 与 `llm_latency` 的 
`type`、`ai_cache_hits_total` 的 `layer`)不可被禁用。 |
 
 将标签值设置为 `""` 时,标签仍保留在指标 schema 中,因此现有的仪表盘、`absent()` 告警和 recording rule 
都不受影响——只是将仅因这些标签而不同的高基数时间序列合并为一条。这在 Kubernetes 弹性伸缩等动态环境中尤其有用:此时上游节点 IP(`node` 
标签)频繁变化,否则会很快撑爆 `prometheus-metrics` 共享字典。
 
@@ -249,6 +249,30 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
 | request_type       | traditional_http / ai_chat / ai_stream                  
                                                                        |
 | llm_model       | 对于非传统的 http 请求,llm 模型的名称                                   
                                                       |
 
+### `apisix_ai_cache_*` 系列指标的标签
+
+[`ai-cache`](./ai-cache.md) 插件导出以下四个指标:
+
+- `apisix_ai_cache_hits_total`:统计由缓存命中并直接返回的请求数,按命中的缓存层区分。
+- `apisix_ai_cache_misses_total`:统计经过插件查询但未命中缓存的请求数。
+- `apisix_ai_cache_bypasses_total`:统计完全绕过缓存查询的请求数。
+- `apisix_ai_cache_embedding_latency`:语义层发起的 embedding 调用延迟(毫秒)的直方图,围绕 
embedding 服务的完整往返计时,成功与失败的调用均会记录。
+
+它们共享以下标签:
+
+| 名称 | 描述 |
+| ---------- | 
-----------------------------------------------------------------------------------------------------------------------------
 |
+| layer      | 仅存在于 `apisix_ai_cache_hits_total`。命中的缓存层:`exact` 或 `semantic`。  
                                                                               |
+| route      | 指标对应的路由名称。如果路由未配置名称或请求不匹配任何路由,则默认为空字符串。                         
|
+| route_id      | 指标对应的路由 ID。如果请求不匹配任何路由,则默认为空字符串。                         |
+| service    | 匹配路由所属的服务名称。如果匹配的路由不属于任何服务,则默认为空字符串。 |
+| service_id    | 匹配路由所属的服务 ID。如果匹配的路由不属于任何服务,则默认为空字符串。 |
+| consumer   | 与请求关联的消费者名称。如果请求没有与之关联的消费者,则默认为空字符串。                       |
+| node       | `ai-proxy` 或 `ai-proxy-multi` 插件选中的 LLM 实例名称,例如 
`ai-proxy-openai`。这些插件上报的是实例名称而非上游 IP 地址,缓存命中与未命中时均是如此。                         
                                                                 |
+| request_type       | traditional_http / ai_chat / ai_stream                  
                                                                        |
+| request_llm_model       | 客户端请求的模型名称。                                        
                                                  |
+| llm_model       | LLM 响应中报告的模型名称。缓存命中的请求不会到达 LLM,此标签为空字符串。                   
                                                                       |
+
 ### `apisix_http_latency` 的标签
 
 以下标签用于区分 `apisix_http_latency` 指标。
diff --git a/t/plugin/prometheus-ai-cache.t b/t/plugin/prometheus-ai-cache.t
new file mode 100644
index 000000000..31e07f2ea
--- /dev/null
+++ b/t/plugin/prometheus-ai-cache.t
@@ -0,0 +1,270 @@
+#
+# 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.
+#
+BEGIN {
+    if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+        $SkipReason = "unavailable for the hup tests";
+
+    } else {
+        $ENV{TEST_NGINX_USE_HUP} = 1;
+        undef $ENV{TEST_NGINX_USE_STAP};
+    }
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    my $extra_yaml_config = <<_EOC_;
+plugin_attr:
+    prometheus:
+        refresh_interval: 0.1
+plugins:
+  - ai-proxy
+  - ai-cache
+  - prometheus
+  - public-api
+_EOC_
+    $block->set_value("extra_yaml_config", $extra_yaml_config);
+
+    my $http_config = <<_EOC_;
+    server {
+        listen 6724;
+        default_type 'application/json';
+
+        location /v1/embeddings {
+            content_by_lua_block { require("lib.ai_cache_mock").embeddings() }
+        }
+    }
+_EOC_
+    $block->set_value("http_config", $http_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: create a route with ai-proxy, ai-cache and the metrics public-api 
route
+--- config
+    location /t {
+        content_by_lua_block {
+            require("lib.test_redis").flush_port("127.0.0.1", 6379)
+
+            local data = {
+                {
+                    url = "/apisix/admin/routes/1",
+                    data = [[{
+                        "uri": "/chat",
+                        "name": "ai-cache-route",
+                        "plugins": {
+                            "prometheus": {},
+                            "ai-proxy": {
+                                "provider": "openai",
+                                "auth": { "header": { "Authorization": "Bearer 
test-key" } },
+                                "options": { "model": "gpt-4o" },
+                                "override": { "endpoint": 
"http://127.0.0.1:1980"; }
+                            },
+                            "ai-cache": {
+                                "redis_host": "127.0.0.1",
+                                "redis_port": 6379,
+                                "layers": ["exact", "semantic"],
+                                "bypass_on": [{"header": "X-No-Cache", 
"equals": "1"}],
+                                "semantic": {
+                                    "similarity_threshold": 0.9,
+                                    "embedding": {
+                                        "openai": {
+                                            "endpoint": 
"http://127.0.0.1:6724/v1/embeddings";,
+                                            "model": "text-embedding-3-small",
+                                            "api_key": "test-key"
+                                        }
+                                    },
+                                    "vector_search": { "redis": {} }
+                                }
+                            }
+                        }
+                    }]],
+                },
+                {
+                    url = "/apisix/admin/routes/metrics",
+                    data = [[{
+                        "plugins": {
+                            "public-api": {}
+                        },
+                        "uri": "/apisix/prometheus/metrics"
+                    }]]
+                },
+            }
+
+            local t = require("lib.test_admin").test
+
+            for _, d in ipairs(data) do
+                local code, body = t(d.url, ngx.HTTP_PUT, d.data)
+                if code >= 300 then ngx.status = code end
+                ngx.say(body)
+            end
+        }
+    }
+--- response_body eval
+"passed\n" x 2
+
+
+
+=== TEST 2: send a chat request (cache MISS)
+--- request
+POST /chat
+{"model":"gpt-4o","messages":[{"role":"user","content":"What is the capital of 
France?"}]}
+--- more_headers
+X-AI-Fixture: openai/chat-basic.json
+--- response_headers
+X-AI-Cache-Status: MISS
+--- error_code: 200
+--- wait: 0.5
+
+
+
+=== TEST 3: assert ai_cache_misses_total metric
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_misses_total\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model="gpt-4o"\}
 1/
+
+
+
+=== TEST 4: send the same chat request (exact-layer HIT)
+--- request
+POST /chat
+{"model":"gpt-4o","messages":[{"role":"user","content":"What is the capital of 
France?"}]}
+--- response_headers
+X-AI-Cache-Status: HIT
+! X-AI-Cache-Similarity
+--- error_code: 200
+--- wait: 0.3
+
+
+
+=== TEST 5: assert ai_cache_hits_total metric with layer=exact and empty 
llm_model
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_hits_total\{layer="exact",route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model=""\}
 1/
+
+
+
+=== TEST 6: ai_cache_embedding_latency is not recorded for the exact hit
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_unlike eval
+qr/apisix_ai_cache_embedding_latency_count\{[^}]*llm_model=""\}/
+
+
+
+=== TEST 7: send a paraphrased chat request (semantic-layer HIT, cos 0.922 >= 
0.9)
+--- request
+POST /chat
+{"model":"gpt-4o","messages":[{"role":"user","content":"What's the capital 
city of France?"}]}
+--- response_headers_like
+X-AI-Cache-Status: HIT
+X-AI-Cache-Similarity: 0\.92\d\d
+--- error_code: 200
+--- wait: 0.3
+
+
+
+=== TEST 8: assert ai_cache_hits_total metric with layer=semantic
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_hits_total\{layer="semantic",route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model=""\}
 1/
+
+
+
+=== TEST 9: assert ai_cache_embedding_latency_bucket metric for the miss lookup
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_embedding_latency_bucket\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model="gpt-4o",le="\d+"\}
 1/
+
+
+
+=== TEST 10: assert ai_cache_embedding_latency_count metric for the miss lookup
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_embedding_latency_count\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model="gpt-4o"\}
 1/
+
+
+
+=== TEST 11: assert ai_cache_embedding_latency_sum metric for the miss lookup
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_embedding_latency_sum\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model="gpt-4o"\}
 \d+/
+
+
+
+=== TEST 12: assert ai_cache_embedding_latency_count metric for the 
semantic-hit lookup
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_embedding_latency_count\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model=""\}
 1/
+
+
+
+=== TEST 13: send a chat request with the bypass header (BYPASS)
+--- request
+POST /chat
+{"model":"gpt-4o","messages":[{"role":"user","content":"What is the capital of 
France?"}]}
+--- more_headers
+X-No-Cache: 1
+X-AI-Fixture: openai/chat-basic.json
+--- response_headers
+X-AI-Cache-Status: BYPASS
+--- error_code: 200
+--- wait: 0.3
+
+
+
+=== TEST 14: assert ai_cache_bypasses_total metric
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_ai_cache_bypasses_total\{route="ai-cache-route",route_id="1",.*node="ai-proxy-openai",request_type="ai_chat",request_llm_model="gpt-4o",llm_model="gpt-4o"\}
 1/
+
+
+
+=== TEST 15: reject disabling the structural `layer` label on 
ai_cache_hits_total
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/plugin_metadata/prometheus',
+                ngx.HTTP_PUT,
+                [[{"disabled_labels": {"ai_cache_hits_total": ["layer"]}}]])
+            ngx.say(body)
+        }
+    }
+--- response_body eval
+qr/failed to validate item 1/

Reply via email to