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

baoyuan 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 3487fb6c5 feat: support header prefix in limit-count rules (#13004)
3487fb6c5 is described below

commit 3487fb6c51cc47ac6ad108457513db1fde10c007
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Thu Feb 12 11:47:10 2026 +0545

    feat: support header prefix in limit-count rules (#13004)
---
 apisix/plugins/ai-rate-limiting.lua        |   4 +
 apisix/plugins/limit-count/init.lua        |  35 +++++-
 docs/en/latest/plugins/ai-rate-limiting.md |   1 +
 docs/en/latest/plugins/limit-count.md      |   3 +-
 docs/zh/latest/plugins/ai-rate-limiting.md |   1 +
 docs/zh/latest/plugins/limit-count.md      |   1 +
 t/plugin/ai-rate-limiting.t                | 186 +++++++++++++++++++++++++++++
 t/plugin/limit-count-rules.t               | 154 ++++++++++++++++++++++++
 8 files changed, 378 insertions(+), 7 deletions(-)

diff --git a/apisix/plugins/ai-rate-limiting.lua 
b/apisix/plugins/ai-rate-limiting.lua
index b24c392a8..8c7eea51a 100644
--- a/apisix/plugins/ai-rate-limiting.lua
+++ b/apisix/plugins/ai-rate-limiting.lua
@@ -94,6 +94,10 @@ local schema = {
                         },
                     },
                     key = {type = "string"},
+                    header_prefix = {
+                        type = "string",
+                        description = "prefix for rate limit headers"
+                    },
                 },
                 required = {"count", "time_window", "key"},
             },
diff --git a/apisix/plugins/limit-count/init.lua 
b/apisix/plugins/limit-count/init.lua
index 1a1427e10..705bf658c 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -101,6 +101,10 @@ local schema = {
                         },
                     },
                     key = {type = "string"},
+                    header_prefix = {
+                        type = "string",
+                        description = "prefix for rate limit headers"
+                    },
                 },
                 required = {"count", "time_window", "key"},
             },
@@ -308,7 +312,7 @@ local function get_rules(ctx, conf)
     end
 
     local rules = {}
-    for _, rule in ipairs(conf.rules) do
+    for index, rule in ipairs(conf.rules) do
         local count, err = resolve_var(ctx, rule.count)
         if err then
             goto CONTINUE
@@ -326,6 +330,7 @@ local function get_rules(ctx, conf)
             time_window = time_window,
             key_type = "constant",
             key = key,
+            header_prefix = rule.header_prefix or index,
         })
 
         ::CONTINUE::
@@ -334,6 +339,28 @@ local function get_rules(ctx, conf)
 end
 
 
+
+local function construct_rate_limiting_headers(conf, name, rule, metadata)
+    local prefix = "X-"
+    if name == "ai-rate-limiting" then
+        prefix = "X-AI-"
+    end
+
+    if rule.header_prefix then
+        return {
+            limit_header = prefix .. rule.header_prefix .. "-RateLimit-Limit",
+            remaining_header = prefix .. rule.header_prefix .. 
"-RateLimit-Remaining",
+            reset_header = prefix .. rule.header_prefix .. "-RateLimit-Reset",
+        }
+    end
+    return  {
+        limit_header = conf.limit_header or metadata.limit_header,
+        remaining_header = conf.remaining_header or metadata.remaining_header,
+        reset_header = conf.reset_header or metadata.reset_header,
+    }
+end
+
+
 local function run_rate_limit(conf, rule, ctx, name, cost, dry_run)
     local lim, err = create_limit_obj(conf, rule, name)
 
@@ -387,11 +414,7 @@ local function run_rate_limit(conf, rule, ctx, name, cost, 
dry_run)
     end
     core.log.info("limit-count plugin-metadata: ", 
core.json.delay_encode(metadata))
 
-    local set_limit_headers = {
-        limit_header = conf.limit_header or metadata.limit_header,
-        remaining_header = conf.remaining_header or metadata.remaining_header,
-        reset_header = conf.reset_header or metadata.reset_header,
-    }
+    local set_limit_headers = construct_rate_limiting_headers(conf, name, 
rule, metadata)
     local phase = get_phase()
     local set_header = phase ~= "log"
 
diff --git a/docs/en/latest/plugins/ai-rate-limiting.md 
b/docs/en/latest/plugins/ai-rate-limiting.md
index fd2f68730..41861387a 100644
--- a/docs/en/latest/plugins/ai-rate-limiting.md
+++ b/docs/en/latest/plugins/ai-rate-limiting.md
@@ -47,6 +47,7 @@ The `ai-rate-limiting` Plugin enforces token-based rate 
limiting for requests se
 | rules.count                  | integer or string | True  |          | >0 or 
variable expression                              | The maximum number of tokens 
allowed within a given time interval. Can be a static integer or a variable 
expression like `$http_custom_limit`. |
 | rules.time_window            | integer or string | True  |          | >0 or 
variable expression                              | The time interval 
corresponding to the rate limiting `count` in seconds. Can be a static integer 
or a variable expression. |
 | rules.key                    | string         | True     |          |        
                                                 | The key to count requests 
by. If the configured key does not exist, the rule will not be executed. The 
`key` is interpreted as a combination of variables, for example: 
`$http_custom_a $http_custom_b`. |
+| rules.header_prefix          | string         | False    |          |        
                                                 | Prefix for rate limit 
headers. If configured, the response will include 
`X-{header_prefix}-RateLimit-Limit`, `X-{header_prefix}-RateLimit-Remaining`, 
and `X-{header_prefix}-RateLimit-Reset` headers. If not configured, the index 
of the rule in the rules array is used as the prefix. For example, headers for 
the first rule will be `X-1-RateLimit-Limit`, `X-1-Rate [...]
 | show_limit_quota_header      | boolean        | False    | true     |        
                                                 | If true, includes 
`X-AI-RateLimit-Limit-*`, `X-AI-RateLimit-Remaining-*`, and 
`X-AI-RateLimit-Reset-*` headers in the response, where `*` is the instance 
name. |
 | limit_strategy               | string         | False    | total_tokens | 
[total_tokens, prompt_tokens, completion_tokens] | Type of token to apply rate 
limiting. `total_tokens` is the sum of `prompt_tokens` and `completion_tokens`. 
|
 | instances                    | array[object]  | False    |          |        
                                                 | LLM instance rate limiting 
configurations. |
diff --git a/docs/en/latest/plugins/limit-count.md 
b/docs/en/latest/plugins/limit-count.md
index cba8f8d6a..8b631750c 100644
--- a/docs/en/latest/plugins/limit-count.md
+++ b/docs/en/latest/plugins/limit-count.md
@@ -49,7 +49,8 @@ You may see the following rate limiting headers in the 
response:
 | rules                   | array[object] | False                              
 |               |                            | A list of rate limiting rules. 
Each rule is an object containing `count`, `time_window`, and `key`.            
                                                                                
                                                                                
                                    |
 | rules.count             | integer | True                                     
 |               | > 0                        | The maximum number of requests 
allowed within a given time interval.                                           
                                                                                
                                                                                
                                  |
 | rules.time_window       | integer | True                                     
 |               | > 0                        | The time interval corresponding 
to the rate limiting `count` in seconds.                                        
                                                                                
                                                                                
                                 |
-| rules.key               | string  | True                                     
 |               |                            | The key to count requests by. 
If the configured key does not exist, the rule will not be executed. The `key` 
is interpreted as a combination of variables, for example: `$http_custom_a 
$http_custom_b`.                                                                
                                                                                
                      [...]
+| rules.key               | string  | True                                     
 |               |                            | The key to count requests by. 
If the configured key does not exist, the rule will not be executed. The `key` 
is interpreted as a combination of variables, for example: `$http_custom_a 
$http_custom_b`.                                                                
                                                                                
                      [...]
+| rules.header_prefix     | string  | False                                    
 |               |                            | Prefix for rate limit headers. 
If configured, the response will include `X-{header_prefix}-RateLimit-Limit`, 
`X-{header_prefix}-RateLimit-Remaining`, and 
`X-{header_prefix}-RateLimit-Reset` headers. If not configured, the index of 
the rule in the rules array is used as the prefix. For example, headers for the 
first rule will be `X-1-RateLimit-Limit`, `X-1-RateLim [...]
 | key_type                | string  | False                                    
 | var         | ["var","var_combination","constant"] | The type of key. If the 
`key_type` is `var`, the `key` is interpreted a variable. If the `key_type` is 
`var_combination`, the `key` is interpreted as a combination of variables. If 
the `key_type` is `constant`, the `key` is interpreted as a constant.           
       |
 | key                     | string  | False                                    
 | remote_addr |                                        | The key to count 
requests by. If the `key_type` is `var`, the `key` is interpreted a variable. 
The variable does not need to be prefixed by a dollar sign (`$`). If the 
`key_type` is `var_combination`, the `key` is interpreted as a combination of 
variables. All variables should be prefixed by dollar signs (`$`). For example, 
to configure the `key` to use [...]
 | rejected_code           | integer | False                                    
 | 503           | [200,...,599]                          | The HTTP status 
code returned when a request is rejected for exceeding the threshold.           
                                                                                
                                                                                
                                                                                
                  [...]
diff --git a/docs/zh/latest/plugins/ai-rate-limiting.md 
b/docs/zh/latest/plugins/ai-rate-limiting.md
index c6a719c2a..42ad20a7a 100644
--- a/docs/zh/latest/plugins/ai-rate-limiting.md
+++ b/docs/zh/latest/plugins/ai-rate-limiting.md
@@ -47,6 +47,7 @@ description: ai-rate-limiting 插件对发送到 LLM 服务的请求实施基于
 | rules.count                  | integer 或 string | 是  |          | >0 或变量表达式  
                            | 在给定时间间隔内允许的最大令牌数。可以是静态整数或变量表达式,如 
`$http_custom_limit`。 |
 | rules.time_window            | integer 或 string | 是  |          | >0 或变量表达式  
                            | 与速率限制 `count` 对应的时间间隔(秒)。可以是静态整数或变量表达式。 |
 | rules.key                    | string         | 是     |          |           
                                              | 
用于计数请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量组合,例如:`$http_custom_a $http_custom_b`。 |
+| rules.header_prefix          | string         | 否    |          |            
                                             | 速率限制头部的前缀。如果配置了此项,响应将包含 
`X-AI-{header_prefix}-RateLimit-Limit`、`X-AI-{header_prefix}-RateLimit-Remaining`
 和 `X-AI-{header_prefix}-RateLimit-Reset` 头部。如果未配置,将使用规则索引 (从 1 开始) 作为前缀。|
 | show_limit_quota_header      | boolean        | 否    | true     |            
                                             | 如果为 true,则在响应中包含 
`X-AI-RateLimit-Limit-*`、`X-AI-RateLimit-Remaining-*` 和 
`X-AI-RateLimit-Reset-*` 头部,其中 `*` 是实例名称。 |
 | limit_strategy               | string         | 否    | total_tokens | 
[total_tokens, prompt_tokens, completion_tokens] | 应用速率限制的令牌类型。`total_tokens` 是 
`prompt_tokens` 和 `completion_tokens` 的总和。 |
 | instances                    | array[object]  | 否    |          |            
                                             | LLM 实例速率限制配置。 |
diff --git a/docs/zh/latest/plugins/limit-count.md 
b/docs/zh/latest/plugins/limit-count.md
index df4acfa71..58dc20dbf 100644
--- a/docs/zh/latest/plugins/limit-count.md
+++ b/docs/zh/latest/plugins/limit-count.md
@@ -51,6 +51,7 @@ description: limit-count 插件使用固定窗口算法,通过给定时间间
 | rules.count | integer | 是 | | > 0 | 给定时间间隔内允许的最大请求数。 |
 | rules.time_window | integer | 是 | | > 0 | 速率限制 `count` 对应的时间间隔(以秒为单位)。 |
 | rules.key | string | 是 | | | 用于统计请求的键。如果配置的键不存在,则不会执行该规则。`key` 
被解释为变量的组合,例如:`$http_custom_a $http_custom_b`。|
+| rules.header_prefix | string | 否 | | | 速率限制标头的前缀。如果已配置,响应将包含 
`X-{header_prefix}-RateLimit-Limit`、`X-{header_prefix}-RateLimit-Remaining` 和 
`X-{header_prefix}-RateLimit-Reset` 标头。如果未配置,则使用规则数组中规则的索引作为前缀。例如,第一个规则的标头将是 
`X-1-RateLimit-Limit`、`X-1-RateLimit-Remaining` 和 `X-1-RateLimit-Reset`。|
 | key_type | string | 否 | var | ["var","var_combination","constant"] | key 
的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 
`key` 将被解释为变量的组合。如果 `key_type` 为 `constant`,则 `key` 将被解释为常量。 |
 | key | string | 否 | remote_addr | | 用于计数请求的 key。如果 `key_type` 为 `var`,则 `key` 
将被解释为变量。变量不需要以美元符号(`$`)为前缀。如果 `key_type` 为 `var_combination`,则 `key` 
会被解释为变量的组合。所有变量都应该以美元符号 (`$`) 为前缀。例如,要配置 `key` 使用两个请求头 `custom-a` 和 `custom-b` 
的组合,则 `key` 应该配置为 `$http_custom_a $http_custom_b`。如果 `key_type` 为 `constant`,则 
`key` 会被解释为常量值。|
 | rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 
状态代码。|
diff --git a/t/plugin/ai-rate-limiting.t b/t/plugin/ai-rate-limiting.t
index f0ae0462c..21283e94a 100644
--- a/t/plugin/ai-rate-limiting.t
+++ b/t/plugin/ai-rate-limiting.t
@@ -1373,3 +1373,189 @@ GET /t
 passed
 --- error_log
 failed to get rate limit rules
+
+
+
+=== TEST 27: setup route with rules without header prefix
+--- 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": "/ai",
+                    "plugins": {
+                        "ai-proxy-multi": {
+                            "instances": [
+                                {
+                                    "name": "deepseek",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token"
+                                        }
+                                    },
+                                    "override": {
+                                        "endpoint": "http://localhost:16724";
+                                    }
+                                }
+                            ],
+                            "ssl_verify": false
+                        },
+                        "ai-rate-limiting": {
+                            "rejected_code": 429,
+                            "rules": [
+                                {
+                                    "count": 20,
+                                    "time_window": 10,
+                                    "key": "${http_user}"
+                                },
+                                {
+                                    "count": "${http_count ?? 30}",
+                                    "time_window": "${http_window ?? 10}",
+                                    "key": "${http_project}"
+                                }
+                            ]
+                        }
+                    },
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "canbeanything.com": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 28: request to confirm headers with rule index are sent - target user
+--- request
+POST /ai
+{"messages":[{"role":"system","content":"Youareamathematician"},{"role":"user","content":"Whatis1+1?"}]}
+--- more_headers
+user: jack
+--- response_headers
+X-AI-1-RateLimit-Limit: 20
+X-AI-1-RateLimit-Remaining: 19
+X-AI-1-RateLimit-Reset: 10
+
+
+
+=== TEST 29: request to confirm headers with rule index are sent - target 
project
+--- request
+POST /ai
+{"messages":[{"role":"system","content":"Youareamathematician"},{"role":"user","content":"Whatis1+1?"}]}
+--- more_headers
+project: apisix
+--- response_headers
+X-AI-2-RateLimit-Limit: 30
+X-AI-2-RateLimit-Remaining: 29
+X-AI-2-RateLimit-Reset: 10
+
+
+
+=== TEST 30: setup route with rules with header prefix
+--- 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": "/ai",
+                    "plugins": {
+                        "ai-proxy-multi": {
+                            "instances": [
+                                {
+                                    "name": "deepseek",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token"
+                                        }
+                                    },
+                                    "override": {
+                                        "endpoint": "http://localhost:16724";
+                                    }
+                                }
+                            ],
+                            "ssl_verify": false
+                        },
+                        "ai-rate-limiting": {
+                            "rejected_code": 429,
+                            "rules": [
+                                {
+                                    "count": 20,
+                                    "time_window": 10,
+                                    "key": "${http_user}",
+                                    "header_prefix": "user"
+                                },
+                                {
+                                    "count": "${http_count ?? 30}",
+                                    "time_window": "${http_window ?? 10}",
+                                    "key": "${http_project}",
+                                    "header_prefix": "project"
+                                }
+                            ]
+                        }
+                    },
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "canbeanything.com": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 31: request to confirm headers with rule header prefix are sent - 
target user
+--- request
+POST /ai
+{"messages":[{"role":"system","content":"Youareamathematician"},{"role":"user","content":"Whatis1+1?"}]}
+--- more_headers
+user: jack
+--- response_headers
+X-AI-User-RateLimit-Limit: 20
+X-AI-User-RateLimit-Remaining: 19
+X-AI-User-RateLimit-Reset: 10
+
+
+
+=== TEST 32: request to confirm headers with rule header prefix are sent - 
target project
+--- request
+POST /ai
+{"messages":[{"role":"system","content":"Youareamathematician"},{"role":"user","content":"Whatis1+1?"}]}
+--- more_headers
+project: apisix
+--- response_headers
+X-AI-Project-RateLimit-Limit: 30
+X-AI-Project-RateLimit-Remaining: 29
+X-AI-Project-RateLimit-Reset: 10
diff --git a/t/plugin/limit-count-rules.t b/t/plugin/limit-count-rules.t
index cda541e65..fbd1dcb43 100644
--- a/t/plugin/limit-count-rules.t
+++ b/t/plugin/limit-count-rules.t
@@ -378,3 +378,157 @@ passed
 GET /t
 --- response_body
 passed
+
+
+
+=== TEST 12: setup route with header_prefix
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "limit-count": {
+                                "rejected_code": 503,
+                                "rejected_msg" : "rejected",
+                                "rules": [
+                                    {
+                                        "key": "${http_user}",
+                                        "count": "${http_jack_count}",
+                                        "time_window": 60,
+                                        "header_prefix": "jack"
+                                    },
+                                    {
+                                        "key": "${http_project}",
+                                        "count": "${http_apisix_count}",
+                                        "time_window": 60,
+                                        "header_prefix": "bar"
+                                    }
+                                ]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 13: match jack
+--- request
+GET /hello
+--- more_headers
+user: jack
+jack-count: 2
+--- error_code: 200
+--- response_headers
+X-Jack-RateLimit-Limit: 2
+X-Jack-RateLimit-Remaining: 1
+X-Jack-RateLimit-Reset: 60
+
+
+
+=== TEST 14: match bar
+--- request
+GET /hello
+--- more_headers
+project: apisix
+apisix-count: 3
+--- error_code: 200
+--- response_headers
+X-Bar-RateLimit-Limit: 3
+X-Bar-RateLimit-Remaining: 2
+X-Bar-RateLimit-Reset: 60
+
+
+
+=== TEST 15: setup route without header_prefix
+--- 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,
+                 [[{
+                        "methods": ["GET"],
+                        "plugins": {
+                            "limit-count": {
+                                "rejected_code": 503,
+                                "rejected_msg" : "rejected",
+                                "rules": [
+                                    {
+                                        "key": "${http_user}",
+                                        "count": "${http_jack_count}",
+                                        "time_window": 60
+                                    },
+                                    {
+                                        "key": "${http_project}",
+                                        "count": "${http_apisix_count}",
+                                        "time_window": 60
+                                    }
+                                ]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 16: match jack
+--- request
+GET /hello
+--- more_headers
+user: jack
+jack-count: 2
+--- error_code: 200
+--- response_headers
+X-1-RateLimit-Limit: 2
+X-1-RateLimit-Remaining: 1
+X-1-RateLimit-Reset: 60
+
+
+
+=== TEST 17: match bar
+--- request
+GET /hello
+--- more_headers
+project: apisix
+apisix-count: 3
+--- error_code: 200
+--- response_headers
+X-2-RateLimit-Limit: 3
+X-2-RateLimit-Remaining: 2
+X-2-RateLimit-Reset: 60

Reply via email to