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

shreemaanabhishek 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 21c7b9152 feat: support rules in limit-conn and ai-rate-limiting 
(#13000)
21c7b9152 is described below

commit 21c7b915240bb6812c665167374cebd2b488c99a
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Wed Feb 11 14:13:41 2026 +0545

    feat: support rules in limit-conn and ai-rate-limiting (#13000)
---
 apisix/plugins/ai-rate-limiting.lua        | 101 +++++++++++----
 apisix/plugins/limit-conn.lua              |  33 ++++-
 apisix/plugins/limit-conn/init.lua         | 116 +++++++++++++----
 docs/en/latest/plugins/ai-rate-limiting.md |   8 +-
 docs/en/latest/plugins/limit-conn.md       |  10 +-
 docs/zh/latest/plugins/ai-rate-limiting.md |   8 +-
 docs/zh/latest/plugins/limit-conn.md       |  12 +-
 t/plugin/ai-rate-limiting.t                | 199 +++++++++++++++++++++++++++++
 t/plugin/limit-conn-variable.t             | 185 ++++++++++++++++++++++++++-
 t/plugin/limit-conn.t                      |   8 +-
 t/plugin/limit-req.t                       |   5 +-
 11 files changed, 608 insertions(+), 77 deletions(-)

diff --git a/apisix/plugins/ai-rate-limiting.lua 
b/apisix/plugins/ai-rate-limiting.lua
index 6f4ce14b1..b24c392a8 100644
--- a/apisix/plugins/ai-rate-limiting.lua
+++ b/apisix/plugins/ai-rate-limiting.lua
@@ -76,17 +76,46 @@ local schema = {
         rejected_msg = {
             type = "string", minLength = 1
         },
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    count = {
+                        oneOf = {
+                            {type = "integer", exclusiveMinimum = 0},
+                            {type = "string"},
+                        },
+                    },
+                    time_window = {
+                        oneOf = {
+                            {type = "integer", exclusiveMinimum = 0},
+                            {type = "string"},
+                        },
+                    },
+                    key = {type = "string"},
+                },
+                required = {"count", "time_window", "key"},
+            },
+        },
     },
     dependencies = {
         limit = {"time_window"},
         time_window = {"limit"}
     },
-    anyOf = {
+    oneOf = {
         {
-            required = {"limit", "time_window"}
+            anyOf = {
+                {
+                    required = {"limit", "time_window"}
+                },
+                {
+                    required = {"instances"}
+                }
+            }
         },
         {
-            required = {"instances"}
+            required = {"rules"},
         }
     }
 }
@@ -109,6 +138,26 @@ end
 
 
 local function transform_limit_conf(plugin_conf, instance_conf, instance_name)
+    local limit_conf = {
+        rejected_code = plugin_conf.rejected_code,
+        rejected_msg = plugin_conf.rejected_msg,
+        show_limit_quota_header = plugin_conf.show_limit_quota_header,
+
+        -- we may expose those fields to ai-rate-limiting later
+        policy = "local",
+        key_type = "constant",
+        allow_degradation = false,
+        sync_interval = -1,
+        limit_header = "X-AI-RateLimit-Limit",
+        remaining_header = "X-AI-RateLimit-Remaining",
+        reset_header = "X-AI-RateLimit-Reset",
+    }
+    if plugin_conf.rules and #plugin_conf.rules > 0 then
+        limit_conf.rules = plugin_conf.rules
+        limit_conf._meta = plugin_conf._meta
+        return limit_conf
+    end
+
     local key = plugin_name .. "#global"
     local limit = plugin_conf.limit
     local time_window = plugin_conf.time_window
@@ -119,25 +168,15 @@ local function transform_limit_conf(plugin_conf, 
instance_conf, instance_name)
         limit = instance_conf.limit
         time_window = instance_conf.time_window
     end
-    return {
-        _vid = key,
-
-        key = key,
-        _meta = plugin_conf._meta,
-        count = limit,
-        time_window = time_window,
-        rejected_code = plugin_conf.rejected_code,
-        rejected_msg = plugin_conf.rejected_msg,
-        show_limit_quota_header = plugin_conf.show_limit_quota_header,
-        -- limit-count need these fields
-        policy = "local",
-        key_type = "constant",
-        allow_degradation = false,
-
-        limit_header = "X-AI-RateLimit-Limit-" .. name,
-        remaining_header = "X-AI-RateLimit-Remaining-" .. name,
-        reset_header = "X-AI-RateLimit-Reset-" .. name,
-    }
+    limit_conf._vid = key
+    limit_conf.key = key
+    limit_conf._meta = plugin_conf._meta
+    limit_conf.count = limit
+    limit_conf.time_window = time_window
+    limit_conf.limit_header = "X-AI-RateLimit-Limit-" .. name
+    limit_conf.remaining_header = "X-AI-RateLimit-Remaining-" .. name
+    limit_conf.reset_header = "X-AI-RateLimit-Reset-" .. name
+    return limit_conf
 end
 
 
@@ -168,8 +207,13 @@ function _M.access(conf, ctx)
         return
     end
 
-    local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs, 
conf)
-    local limit_conf = limit_conf_kvs[ai_instance_name]
+    local limit_conf
+    if conf.rules and #conf.rules > 0 then
+        limit_conf = transform_limit_conf(conf)
+    else
+        local limit_conf_kvs = limit_conf_cache(conf, nil, 
fetch_limit_conf_kvs, conf)
+        limit_conf = limit_conf_kvs[ai_instance_name]
+    end
     if not limit_conf then
         return
     end
@@ -243,8 +287,13 @@ function _M.log(conf, ctx)
 
     core.log.info("instance name: ", instance_name, " used tokens: ", 
used_tokens)
 
-    local limit_conf_kvs = limit_conf_cache(conf, nil, fetch_limit_conf_kvs, 
conf)
-    local limit_conf = limit_conf_kvs[instance_name]
+    local limit_conf
+    if conf.rules and #conf.rules > 0 then
+        limit_conf = transform_limit_conf(conf)
+    else
+        local limit_conf_kvs = limit_conf_cache(conf, nil, 
fetch_limit_conf_kvs, conf)
+        limit_conf = limit_conf_kvs[instance_name]
+    end
     if limit_conf then
         limit_count.rate_limit(limit_conf, ctx, plugin_name, used_tokens)
     end
diff --git a/apisix/plugins/limit-conn.lua b/apisix/plugins/limit-conn.lua
index dd88162af..7b79ac20a 100644
--- a/apisix/plugins/limit-conn.lua
+++ b/apisix/plugins/limit-conn.lua
@@ -54,9 +54,38 @@ local schema = {
         rejected_msg = {
             type = "string", minLength = 1
         },
-        allow_degradation = {type = "boolean", default = false}
+        allow_degradation = {type = "boolean", default = false},
+        rules = {
+            type = "array",
+            items = {
+                type = "object",
+                properties = {
+                    conn = {
+                        oneOf = {
+                            {type = "integer", exclusiveMinimum = 0},
+                            {type = "string"},
+                        },
+                    },
+                    burst = {
+                        oneOf = {
+                            {type = "integer", minimum = 0},
+                            {type = "string"},
+                        },
+                    },
+                    key = {type = "string"},
+                },
+                required = {"conn", "burst", "key"},
+            },
+        },
+    },
+    oneOf = {
+        {
+            required = {"conn", "burst", "default_conn_delay", "key"},
+        },
+        {
+            required = {"default_conn_delay", "rules"},
+        }
     },
-    required = {"conn", "burst", "default_conn_delay", "key"},
     ["if"] = {
         properties = {
             policy = {
diff --git a/apisix/plugins/limit-conn/init.lua 
b/apisix/plugins/limit-conn/init.lua
index 6c5c823e1..b45ff567c 100644
--- a/apisix/plugins/limit-conn/init.lua
+++ b/apisix/plugins/limit-conn/init.lua
@@ -21,6 +21,7 @@ local sleep = core.sleep
 local tonumber = tonumber
 local type = type
 local tostring = tostring
+local ipairs = ipairs
 local shdict_name = "plugin-limit-conn"
 if ngx.config.subsystem == "stream" then
     shdict_name = shdict_name .. "-stream"
@@ -40,34 +41,75 @@ end
 local _M = {}
 
 
-local function create_limit_obj(ctx, conf)
-    core.log.info("create new limit-conn plugin instance")
-
-    local conn = conf.conn
-    if type(conn) == "string" then
+local function resolve_var(ctx, value)
+    if type(value) == "string" then
         local err, _
-        conn, err, _ = core.utils.resolve_var(conn, ctx.var)
+        value, err, _ = core.utils.resolve_var(value, ctx.var)
         if err then
-            return nil, "could not resolve vars in conn: " .. err
+            return nil, "could not resolve var for value: " .. value .. ", 
err: " .. err
         end
-        conn = tonumber(conn)
-        if not conn then
-            return nil, "resolved conn is not a number: " .. tostring(conn)
+        value = tonumber(value)
+        if not value then
+            return nil, "resolved value is not a number: " .. tostring(value)
         end
     end
+    return value
+end
 
-    local burst = conf.burst
-    if type(burst) == "string" then
-        local err, _
-        burst, err, _ = core.utils.resolve_var(burst, ctx.var)
+
+local function get_rules(ctx, conf)
+    if not conf.rules then
+        local conn, err = resolve_var(ctx, conf.conn)
+        if err then
+            return nil, err
+        end
+        local burst, err2 = resolve_var(ctx, conf.burst)
+        if err2 then
+            return nil, err2
+        end
+        return {
+            {
+                conn = conn,
+                burst = burst,
+                key = conf.key,
+                key_type = conf.key_type,
+            }
+        }
+    end
+
+    local rules = {}
+    for _, rule in ipairs(conf.rules) do
+        local conn, err = resolve_var(ctx, rule.conn)
         if err then
-            return nil, "could not resolve vars in burst: " .. err
+            goto CONTINUE
+        end
+        local burst, err2 = resolve_var(ctx, rule.burst)
+        if err2 then
+            goto CONTINUE
         end
-        burst = tonumber(burst)
-        if not burst then
-            return nil, "resolved burst is not a number: " .. tostring(burst)
+
+        local key, _, n_resolved = core.utils.resolve_var(rule.key, ctx.var)
+        if n_resolved == 0 then
+            goto CONTINUE
         end
+        core.table.insert(rules, {
+            conn = conn,
+            burst = burst,
+            key_type = "constant",
+            key = key,
+        })
+
+        ::CONTINUE::
     end
+    return rules
+end
+
+
+local function create_limit_obj(conf, rule, default_conn_delay)
+    core.log.info("create new limit-conn plugin instance")
+
+    local conn = rule.conn
+    local burst = rule.burst
 
     core.log.info("limit conn: ", conn, ", burst: ", burst)
 
@@ -75,25 +117,24 @@ local function create_limit_obj(ctx, conf)
         core.log.info("create new limit-conn redis plugin instance")
 
         return redis_single_new("plugin-limit-conn", conf, conn, burst,
-                                conf.default_conn_delay)
+                                default_conn_delay)
 
     elseif conf.policy == "redis-cluster" then
 
         core.log.info("create new limit-conn redis-cluster plugin instance")
 
         return redis_cluster_new("plugin-limit-conn", conf, conn, burst,
-                                 conf.default_conn_delay)
+                                 default_conn_delay)
     else
         core.log.info("create new limit-conn plugin instance")
         return limit_conn_new(shdict_name, conn, burst,
-                              conf.default_conn_delay)
+                              default_conn_delay)
     end
 end
 
 
-function _M.increase(conf, ctx)
-    core.log.info("ver: ", ctx.conf_version)
-    local lim, err = create_limit_obj(ctx, conf)
+local function run_limit_conn(conf, rule, ctx)
+    local lim, err = create_limit_obj(conf, rule, conf.default_conn_delay)
     if not lim then
         core.log.error("failed to instantiate a resty.limit.conn object: ", 
err)
         if conf.allow_degradation then
@@ -102,9 +143,9 @@ function _M.increase(conf, ctx)
         return 500
     end
 
-    local conf_key = conf.key
+    local conf_key = rule.key
     local key
-    if conf.key_type == "var_combination" then
+    if rule.key_type == "var_combination" then
         local err, n_resolved
         key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var)
         if err then
@@ -114,6 +155,8 @@ function _M.increase(conf, ctx)
         if n_resolved == 0 then
             key = nil
         end
+    elseif rule.key_type == "constant" then
+        key = conf_key
     else
         key = ctx.var[conf_key]
     end
@@ -157,6 +200,27 @@ function _M.increase(conf, ctx)
 end
 
 
+function _M.increase(conf, ctx)
+    core.log.info("ver: ", ctx.conf_version)
+
+    local rules, err = get_rules(ctx, conf)
+    if not rules or #rules == 0 then
+        core.log.error("failed to get limit conn rules: ", err)
+        if conf.allow_degradation then
+            return
+        end
+        return 500
+    end
+
+    for _, rule in ipairs(rules) do
+        local code, msg = run_limit_conn(conf, rule, ctx)
+        if code then
+            return code, msg
+        end
+    end
+end
+
+
 function _M.decrease(conf, ctx)
     local limit_conn = ctx.limit_conn
     if not limit_conn then
diff --git a/docs/en/latest/plugins/ai-rate-limiting.md 
b/docs/en/latest/plugins/ai-rate-limiting.md
index c05006335..fd2f68730 100644
--- a/docs/en/latest/plugins/ai-rate-limiting.md
+++ b/docs/en/latest/plugins/ai-rate-limiting.md
@@ -41,8 +41,12 @@ The `ai-rate-limiting` Plugin enforces token-based rate 
limiting for requests se
 
 | Name                         | Type            | Required | Default  | Valid 
values                                             | Description |
 
|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|
-| limit                        | integer        | False    |          | >0     
                        | The maximum number of tokens allowed within a given 
time interval. At least one of `limit` and `instances.limit` should be 
configured. |
-| time_window                  | integer        | False    |          | >0     
                        | The time interval corresponding to the rate limiting 
`limit` in seconds. At least one of `time_window` and `instances.time_window` 
should be configured. |
+| limit                        | integer        | False    |          | >0     
                        | The maximum number of tokens allowed within a given 
time interval. At least one of `limit` and `instances.limit` should be 
configured. Required if `rules` is not configured. |
+| time_window                  | integer        | False    |          | >0     
                        | The time interval corresponding to the rate limiting 
`limit` in seconds. At least one of `time_window` and `instances.time_window` 
should be configured. Required if `rules` is not configured. |
+| rules                        | array[object]  | False    |          |        
                                                 | A list of rate limiting 
rules. Each rule is an object containing `count`, `time_window`, and `key`. If 
configured, this takes precedence over `limit` and `time_window`. |
+| 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`. |
 | 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-conn.md 
b/docs/en/latest/plugins/limit-conn.md
index 2d7b07884..1cba1976e 100644
--- a/docs/en/latest/plugins/limit-conn.md
+++ b/docs/en/latest/plugins/limit-conn.md
@@ -38,12 +38,16 @@ The `limit-conn` Plugin limits the rate of requests by the 
number of concurrent
 
 | Name       | Type    | Required | Default     | Valid values      | 
Description     |
 
|------------|---------|----------|-------------|-------------------|-----------------|
-| conn       | integer | True     |     | > 0   | The maximum number of 
concurrent requests allowed. Requests exceeding the configured limit and below 
`conn + burst` will be delayed.      |
-| burst      | integer | True     |     | >= 0        | The number of 
excessive concurrent requests allowed to be delayed per second. Requests 
exceeding the limit will be rejected immediately.       |
+| conn       | integer | False     |     | > 0   | The maximum number of 
concurrent requests allowed. Requests exceeding the configured limit and below 
`conn + burst` will be delayed. Required if `rules` is not configured.      |
+| burst      | integer | False     |     | >= 0        | The number of 
excessive concurrent requests allowed to be delayed per second. Requests 
exceeding the limit will be rejected immediately. Required if `rules` is not 
configured.       |
 | default_conn_delay       | number  | True     |     | > 0    | Processing 
latency allowed in seconds for concurrent requests exceeding `conn + burst`, 
which can be dynamically adjusted based on `only_use_default_delay` setting.    
       |
 | only_use_default_delay   | boolean | False    | false       |      | If 
false, delay requests proportionally based on how much they exceed the `conn` 
limit. The delay grows larger as congestion increases. For instance, with 
`conn` being `5`, `burst` being `3`, and `default_conn_delay` being `1`, 6 
concurrent requests would result in a 1-second delay, 7 requests a 2-second 
delay, 8 requests a 3-second delay, and so on, until the total limit of `conn + 
burst` is reached, beyond which req [...]
+| rules                    | array[object] | False    |       |                
   | A list of connection limiting rules. Each rule is an object containing 
`conn`, `burst`, and `key`. If configured, this takes precedence over `conn`, 
`burst`, and `key`. |
+| rules.conn               | integer or string | True |       | > 0 or 
variable expression | The maximum number of concurrent requests allowed. Can be 
a static integer or a variable expression like `$http_custom_conn`. |
+| rules.burst              | integer or string | True |       | >= 0 or 
variable expression | The number of excessive concurrent requests allowed to be 
delayed. 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`. |
 | key_type        | string  | False      | var   | ["var","var_combination"] | 
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.    |
-| 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 a combination of two request headers `custom-a` 
and `custom-b`, the `key` should  [...]
+| 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 a combination of two request headers `custom-a` 
and `custom-b`, the `key` should  [...]
 | key_ttl   | integer | False      | 3600          |   | The TTL of the Redis 
key in seconds. Used when `policy` is `redis` or `redis-cluster`. |
 | rejected_code   | integer | False      | 503   | [200,...,599]   | The HTTP 
status code returned when a request is rejected for exceeding the threshold.    
 |
 | rejected_msg    | string  | False        |       | non-empty   | The 
response body 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 642077681..c6a719c2a 100644
--- a/docs/zh/latest/plugins/ai-rate-limiting.md
+++ b/docs/zh/latest/plugins/ai-rate-limiting.md
@@ -41,8 +41,12 @@ description: ai-rate-limiting 插件对发送到 LLM 服务的请求实施基于
 
 | 名称                         | 类型            | 必选项 | 默认值  | 有效值                
                             | 描述 |
 
|------------------------------|----------------|----------|----------|---------------------------------------------------------|-------------|
-| limit                        | integer        | 否    |          | >0         
                    | 在给定时间间隔内允许的最大令牌数。`limit` 和 `instances.limit` 中至少应配置一个。 |
-| time_window                  | integer        | 否    |          | >0         
                    | 与速率限制 `limit` 对应的时间间隔(秒)。`time_window` 和 
`instances.time_window` 中至少应配置一个。 |
+| limit                        | integer        | 否    |          | >0         
                    | 在给定时间间隔内允许的最大令牌数。`limit` 和 `instances.limit` 
中至少应配置一个。如果未配置 `rules`,则为必填项。 |
+| time_window                  | integer        | 否    |          | >0         
                    | 与速率限制 `limit` 对应的时间间隔(秒)。`time_window` 和 
`instances.time_window` 中至少应配置一个。如果未配置 `rules`,则为必填项。 |
+| rules                        | array[object]  | 否    |          |            
                                             | 速率限制规则列表。每个规则是一个包含 
`count`、`time_window` 和 `key` 的对象。如果配置了此项,则优先于 `limit` 和 `time_window`。 |
+| 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`。 |
 | 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-conn.md 
b/docs/zh/latest/plugins/limit-conn.md
index 66836a23c..3e796632f 100644
--- a/docs/zh/latest/plugins/limit-conn.md
+++ b/docs/zh/latest/plugins/limit-conn.md
@@ -38,12 +38,16 @@ description: limit-conn 插件通过管理并发连接来限制请求速率。
 
 | 名称        | 类型    | 必选项    | 默认值 | 有效值                      | 描述             
 |
 
|------------|---------|----------|-------|----------------------------|------------------|
-| conn | integer | 是 | | > 0 | 允许的最大并发请求数。超过配置的限制且低于`conn + burst`的请求将被延迟。|
-| burst | integer | 是 | | >= 0 | 每秒允许延迟的过多并发请求数。超过限制的请求将被立即拒绝。|
+| conn | integer | 否 | | > 0 | 允许的最大并发请求数。超过配置的限制且低于`conn + 
burst`的请求将被延迟。如果未配置 `rules`,则为必填项。|
+| burst | integer | 否 | | >= 0 | 每秒允许延迟的过多并发请求数。超过限制的请求将被立即拒绝。如果未配置 
`rules`,则为必填项。|
 | default_conn_delay | number | 是 | | > 0 | 允许超过`conn + 
burst`的并发请求的处理延迟(秒),可根据`only_use_default_delay`设置动态调整。|
 | only_use_default_delay | boolean | 否 | false | | 如果为 
false,则根据请求超出`conn`限制的程度按比例延迟请求。拥塞越严重,延迟就越大。例如,当 `conn` 为 `5`、`burst` 为 `3` 且 
`default_conn_delay` 为 `1` 时,6 个并发请求将导致 1 秒的延迟,7 个请求将导致 2 秒的延迟,8 个请求将导致 3 
秒的延迟,依此类推,直到达到 `conn + burst` 的总限制,超过此限制的请求将被拒绝。如果为 true,则使用 
`default_conn_delay` 延迟 `burst` 范围内的所有超额请求。超出 `conn + burst` 的请求将被立即拒绝。例如,当 
`conn` 为 `5`、`burst` 为 `3` 且 `default_conn_delay` 为 `1` 时,6、7 或 8 个并发请求都将延迟 1 
秒。|
+| rules                    | array[object] | 否    |       |                   
| 连接限制规则列表。每个规则是一个包含 `conn`、`burst` 和 `key` 的对象。如果配置了此项,则优先于 `conn`、`burst` 和 
`key`。 |
+| rules.conn               | integer 或 string | 是 |       | > 0 或变量表达式 | 
允许的最大并发请求数。可以是静态整数或变量表达式,如 `$http_custom_conn`。 |
+| rules.burst              | integer 或 string | 是 |       | >= 0 或变量表达式 | 
允许延迟的过多并发请求数。可以是静态整数或变量表达式。 |
+| rules.key                | string  | 是     |       |                   | 
用于计数请求的键。如果配置的键不存在,则不会执行该规则。`key` 被解释为变量组合,例如:`$http_custom_a $http_custom_b`。 |
 | key_type | string | 否 | var | ["var","var_combination"] | key 
的类型。如果`key_type` 为 `var`,则 `key` 将被解释为变量。如果 `key_type` 为 `var_combination`,则 
`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 | 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`。如果未配置 `rules`,则为必填项。|
 | key_ttl | integer | 否 | 3600 | | Redis 键的 TTL(以秒为单位)。当 `policy` 为 `redis` 或 
`redis-cluster` 时使用。 |
 | rejection_code | integer | 否 | 503 | [200,...,599] | 请求因超出阈值而被拒绝时返回的 HTTP 
状态代码。|
 | rejection_msg | string | 否 | | 非空 | 请求因超出阈值而被拒绝时返回的响应主体。|
@@ -60,7 +64,7 @@ description: limit-conn 插件通过管理并发连接来限制请求速率。
 | redis_keepalive_timeout | integer | 否 | 10000 | ≥ 1000 | 当 `policy` 为 
`redis` 或 `redis-cluster` 时,与 `redis` 或 `redis-cluster` 的空闲连接超时时间,单位为毫秒。|
 | redis_keepalive_pool | integer | 否 | 100 | ≥ 1 | 当 `policy` 为 `redis` 或 
`redis-cluster` 时,与 `redis` 或 `redis-cluster` 的连接池最大连接数。|
 | redis_cluster_nodes | array[string] | 否 | | | 具有至少两个地址的 Redis 群集节点列表。当 
policy 为 redis-cluster 时必填。 |
-redis_cluster_name | string | 否 | | | | Redis 集群的名称。当 `policy` 为 
`redis-cluster` 时必须使用。|
+| redis_cluster_name | string | 否 | | | Redis 集群的名称。当 `policy` 为 
`redis-cluster` 时必须使用。|
 | redis_cluster_ssl | boolean | 否 | false | | 如果为 `true`,当 `policy` 为 
`redis-cluster`时,使用 SSL 连接 Redis 集群。|
 | redis_cluster_ssl_verify | boolean | 否 | false | | 如果为 `true`,当 `policy` 为 
`redis-cluster` 时,验证服务器 SSL 证书。  |
 
diff --git a/t/plugin/ai-rate-limiting.t b/t/plugin/ai-rate-limiting.t
index 48c71d04f..f0ae0462c 100644
--- a/t/plugin/ai-rate-limiting.t
+++ b/t/plugin/ai-rate-limiting.t
@@ -1174,3 +1174,202 @@ picked instance: openai
 picked instance: openai
 picked instance: openai
 picked instance: nil
+
+
+
+=== TEST 24: configure instances and rules at the same time
+--- 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-rate-limiting": {
+                            "limit": "${http_count ?? 10}",
+                            "time_window": "${http_time_window ?? 60}",
+                            "instances": [
+                                {
+                                    "name": "openai",
+                                    "limit": "${http_openai_count ?? 20}",
+                                    "time_window": "${http_time_window ?? 60}"
+                                }
+                            ],
+                            "rules": [
+                                {
+                                    "count": 1,
+                                    "time_window": 10,
+                                    "key": "${http_company}"
+                                }
+                            ]
+                        }
+                    },
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "canbeanything.com": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin ai-rate-limiting 
err: value should match only one schema, but matches both schemas 1 and 2"}
+
+
+
+=== TEST 25: setup route with rules
+--- 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://127.0.0.1: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 26: request to confirm rules work
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+
+            local run_tests = function(name, test_cases)
+                local httpc = http.new()
+                for i, case in ipairs(test_cases) do
+                    case.headers["Content-Type"] = "application/json"
+                    local res = httpc:request_uri(
+                        "http://127.0.0.1:"; .. ngx.var.server_port .. "/ai",
+                        {
+                            method = "POST",
+                            body = [[{
+                                "messages": [
+                                    { "role": "system", "content": "You are a 
mathematician" },
+                                    { "role": "user", "content": "What is 
1+1?" }
+                                ]
+                            }]],
+                            headers = case.headers
+                        }
+                    )
+                    if res.status ~= case.code then
+                        ngx.say(name .. ": " .. i  .. "th request should 
return " .. case.code .. ", but got " .. res.status)
+                        ngx.exit(500)
+                    end
+                    -- Add delay to ensure rate limit counters are updated 
properly
+                    ngx.sleep(0.01)
+                end
+            end
+
+            -- for user rule
+            run_tests("user_rule", {
+                { headers = { ["user"] = "jack" }, code = 200 },
+                { headers = { ["user"] = "jack" }, code = 200 },
+                { headers = { ["user"] = "jack" }, code = 429 },
+                { headers = { ["user"] = "rose" }, code = 200 },
+                { headers = { ["user"] = "rose" }, code = 200 },
+                { headers = { ["user"] = "rose" }, code = 429 },
+            })
+
+            -- for project rule with default variable value
+            run_tests("project_rule_default_value", {
+                { headers = { ["project"] = "apisix" }, code = 200 },
+                { headers = { ["project"] = "apisix" }, code = 200 },
+                { headers = { ["project"] = "apisix" }, code = 200 },
+                { headers = { ["project"] = "apisix" }, code = 429 },
+            })
+
+            -- for project rule with custom variable value
+            run_tests("project_rule_custom_variables", {
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 200 },
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 200 },
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 429 },
+            })
+            ngx.sleep(2.1)
+            run_tests("project_rule_custom_variables2", {
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 200 },
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 200 },
+                { headers = { ["project"] = "linux", ["count"] = "20", 
["window"] = "2" }, code = 429 },
+            })
+
+            -- no rule hit
+            run_tests("no_rules", {
+                { headers = {}, code = 500 },
+            })
+
+            ngx.say("passed")
+        }
+    }
+--- request
+GET /t
+--- timeout: 10
+--- response_body
+passed
+--- error_log
+failed to get rate limit rules
diff --git a/t/plugin/limit-conn-variable.t b/t/plugin/limit-conn-variable.t
index 8c82100e4..e1db32c1e 100644
--- a/t/plugin/limit-conn-variable.t
+++ b/t/plugin/limit-conn-variable.t
@@ -129,8 +129,6 @@ GET /test_concurrency
 503
 503
 503
---- error_log
-limit conn: 5, burst: 2
 
 
 
@@ -151,8 +149,6 @@ conn: 3
 503
 503
 503
---- error_log
-limit conn: 3, burst: 2
 
 
 
@@ -174,5 +170,184 @@ burst: 4
 503
 503
 503
+
+
+
+=== TEST 5: configure conn/burst and rules at same time
+--- 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-conn": {
+                                "conn": 2,
+                                "burst": 1,
+                                "default_conn_delay": 0.01,
+                                "rejected_code": 503,
+                                "key": "remote_addr",
+                                "rules": [
+                                    {
+                                        "conn": 1,
+                                        "burst": 0,
+                                        "key": "${http_company}"
+                                    }
+                                ]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/hello"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.print(body)
+        }
+    }
+--- request
+GET /t
+--- error_code: 400
+--- response_body
+{"error_msg":"failed to check the configuration of plugin limit-conn err: 
value should match only one schema, but matches both schemas 1 and 2"}
+
+
+
+=== TEST 6: setup route with rules
+--- 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-conn": {
+                                "default_conn_delay": 0.01,
+                                "rejected_code": 503,
+                                "rules": [
+                                    {
+                                        "conn": 4,
+                                        "burst": 3,
+                                        "key": "${http_user}"
+                                    },
+                                    {
+                                        "conn": "${http_project_conn ?? 3}",
+                                        "burst": "${http_project_burst ?? 2}",
+                                        "key": "${http_project}"
+                                    }
+                                ]
+                            }
+                        },
+                        "upstream": {
+                            "nodes": {
+                                "127.0.0.1:1980": 1
+                            },
+                            "type": "roundrobin"
+                        },
+                        "uri": "/limit_conn"
+                }]]
+                )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 7: request matching user rule
+--- request
+GET /test_concurrency
+--- more_headers
+user: jack
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+200
+200
+503
+503
+503
+
+
+
+=== TEST 8: request matching project rule with default conn/burst
+--- request
+GET /test_concurrency
+--- more_headers
+project: apisix
+--- timeout: 10s
+--- response_body
+200
+200
+200
+200
+200
+503
+503
+503
+503
+503
+
+
+
+=== TEST 9: request matching project rule with custom conn/burst
+--- request
+GET /test_concurrency
+--- more_headers
+project: apisix
+project-conn: 2
+project-burst: 1
+--- timeout: 10s
+--- response_body
+200
+200
+200
+503
+503
+503
+503
+503
+503
+503
+
+
+
+=== TEST 10: request not matching any rule
+--- request
+GET /test_concurrency
+--- timeout: 10s
+--- response_body
+500
+500
+500
+500
+500
+500
+500
+500
+500
+500
 --- error_log
-limit conn: 3, burst: 4
+failed to get limit conn rules
diff --git a/t/plugin/limit-conn.t b/t/plugin/limit-conn.t
index 364282f49..93c69730f 100644
--- a/t/plugin/limit-conn.t
+++ b/t/plugin/limit-conn.t
@@ -106,7 +106,7 @@ done
 --- request
 GET /t
 --- response_body
-property "burst" is required
+value should match only one schema, but matches none
 done
 
 
@@ -321,7 +321,7 @@ GET /test_concurrency
 GET /t
 --- error_code: 400
 --- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err: 
property \"conn\" is required"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err: 
value should match only one schema, but matches none"}
 
 
 
@@ -401,7 +401,7 @@ GET /t
 GET /t
 --- error_code: 400
 --- response_body
-{"error_msg":"failed to check the configuration of plugin limit-conn err: 
property \"conn\" is required"}
+{"error_msg":"failed to check the configuration of plugin limit-conn err: 
value should match only one schema, but matches none"}
 
 
 
@@ -1101,7 +1101,7 @@ qr/limit key: consumer_jackroute&consumer\d+/
 --- request
 GET /t
 --- response_body
-property "burst" is required
+value should match only one schema, but matches none
 done
 
 
diff --git a/t/plugin/limit-req.t b/t/plugin/limit-req.t
index 0f46374d0..6cfc7d306 100644
--- a/t/plugin/limit-req.t
+++ b/t/plugin/limit-req.t
@@ -70,10 +70,9 @@ done
     }
 --- request
 GET /t
---- response_body_like eval
-qr/property "(conn|default_conn_delay)" is required
+--- response_body
+value should match only one schema, but matches none
 done
-/
 
 
 


Reply via email to