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 d0692133ec fix(ai-proxy-multi): reject instances that share a name 
(#13851)
d0692133ec is described below

commit d0692133ecf56bf079b6a6f63c1381aee07fcf58
Author: Nic <[email protected]>
AuthorDate: Thu Aug 20 11:12:32 2026 +0800

    fix(ai-proxy-multi): reject instances that share a name (#13851)
---
 apisix/plugins/ai-proxy-multi.lua        |  17 ++++-
 apisix/plugins/ai-proxy/schema.lua       |   5 +-
 docs/en/latest/plugins/ai-proxy-multi.md |   2 +-
 docs/zh/latest/plugins/ai-proxy-multi.md |   2 +-
 t/plugin/ai-proxy-multi.t                | 117 +++++++++++++++++++++++++++++++
 5 files changed, 139 insertions(+), 4 deletions(-)

diff --git a/apisix/plugins/ai-proxy-multi.lua 
b/apisix/plugins/ai-proxy-multi.lua
index 2746a93574..ca92b2ab84 100644
--- a/apisix/plugins/ai-proxy-multi.lua
+++ b/apisix/plugins/ai-proxy-multi.lua
@@ -39,6 +39,7 @@ local ipairs = ipairs
 local type = type
 local string = string
 local sub = string.sub
+local str_format = string.format
 local url = require("socket.url")
 
 local priority_balancer = require("apisix.balancer.priority")
@@ -124,7 +125,21 @@ function _M.check_schema(conf)
         return false, err
     end
 
-    for _, instance in ipairs(conf.instances) do
+    -- `instance.name` is the only identifier the runtime has: it keys the
+    -- balancer nodes, the health checker and the health status, none of which
+    -- include the priority. Two instances sharing a name therefore collapse
+    -- into a single logical node with a single checker, so one instance's
+    -- health decides the other's fate while requests keep using the first
+    -- instance's auth. Reject duplicates instead of routing on an ambiguity.
+    local seen_names = core.table.new(0, #conf.instances)
+    for i, instance in ipairs(conf.instances) do
+        if seen_names[instance.name] then
+            return false, str_format("duplicate instance name '%s' at " ..
+                                     "instances[%d] and instances[%d]",
+                                     instance.name, seen_names[instance.name], 
i)
+        end
+        seen_names[instance.name] = i
+
         local endpoint = instance and instance.override and 
instance.override.endpoint
         if endpoint then
             local scheme, host, _ = endpoint:match(endpoint_regex)
diff --git a/apisix/plugins/ai-proxy/schema.lua 
b/apisix/plugins/ai-proxy/schema.lua
index d5f13751d0..ada2ab4250 100644
--- a/apisix/plugins/ai-proxy/schema.lua
+++ b/apisix/plugins/ai-proxy/schema.lua
@@ -176,7 +176,10 @@ local ai_instance_schema = {
                 type = "string",
                 minLength = 1,
                 maxLength = 100,
-                description = "Name of the AI service instance.",
+                description = "Name of the AI service instance. Must be "
+                    .. "unique within `instances`: it identifies the instance "
+                    .. "in the balancer, in its health checker and in other "
+                    .. "plugins that reference it, such as ai-rate-limiting.",
             },
             provider = {
                 type = "string",
diff --git a/docs/en/latest/plugins/ai-proxy-multi.md 
b/docs/en/latest/plugins/ai-proxy-multi.md
index ea26402360..4c5d774828 100644
--- a/docs/en/latest/plugins/ai-proxy-multi.md
+++ b/docs/en/latest/plugins/ai-proxy-multi.md
@@ -75,7 +75,7 @@ When an instance's `provider` is set to `bedrock`, the Plugin 
expects requests i
 | balancer.hash_on                   | string         | False    |             
                      | [vars, headers, cookie, consumer, vars_combinations] | 
Used when `type` is `chash`. Support hashing on [NGINX 
variables](https://nginx.org/en/docs/varindex.html), headers, cookie, consumer, 
or a combination of [NGINX variables](https://nginx.org/en/docs/varindex.html). 
|
 | balancer.key                       | string         | False    |             
                      |              | Used when `type` is `chash`. When 
`hash_on` is set to `header` or `cookie`, `key` is required. When `hash_on` is 
set to `consumer`, `key` is not required as the consumer name will be used as 
the key automatically. |
 | instances                          | array[object]  | True     |             
                      |              | LLM instance configurations. |
-| instances.name                     | string         | True     |             
                      |              | Name of the LLM service instance. |
+| instances.name                     | string         | True     |             
                      |              | Name of the LLM service instance. It 
must be unique within `instances`, since it identifies the instance in the 
balancer, in its health checker, and in other Plugins that reference it, such 
as `ai-rate-limiting`. |
 | instances.provider                 | string         | True     |             
                      | [openai, deepseek, azure-openai, aimlapi, anthropic, 
openrouter, gemini, vertex-ai, bedrock, openai-compatible] | LLM service 
provider. When set to `openai`, the Plugin will proxy the request to 
`api.openai.com`. When set to `deepseek`, the Plugin will proxy the request to 
`api.deepseek.com`. When set to `aimlapi`, the Plugin uses the 
OpenAI-compatible driver and proxies the request to  [...]
 | instances.provider_conf            | object         | False     |            
                       |              | Configuration for the specific 
provider. Required when `provider` is set to `vertex-ai` and `override` is not 
configured. Required when `provider` is set to `bedrock`. |
 | instances.provider_conf.project_id | string         | True     |             
                      |              | Google Cloud Project ID. |
diff --git a/docs/zh/latest/plugins/ai-proxy-multi.md 
b/docs/zh/latest/plugins/ai-proxy-multi.md
index 686ad7459e..d056497bbf 100644
--- a/docs/zh/latest/plugins/ai-proxy-multi.md
+++ b/docs/zh/latest/plugins/ai-proxy-multi.md
@@ -75,7 +75,7 @@ import TabItem from '@theme/TabItem';
 | balancer.hash_on                   | string         | 否    |                 
                  | [vars, headers, cookie, consumer, vars_combinations] | 当 
`type` 为 `chash` 时使用。支持基于 [NGINX 
变量](https://nginx.org/en/docs/varindex.html)、标头、cookie、消费者或 [NGINX 
变量](https://nginx.org/en/docs/varindex.html)组合进行哈希。 |
 | balancer.key                       | string         | 否    |                 
                  |              | 当 `type` 为 `chash` 时使用。当 `hash_on` 设置为 
`header` 或 `cookie` 时,需要 `key`。当 `hash_on` 设置为 `consumer` 时,不需要 
`key`,因为消费者名称将自动用作键。 |
 | instances                          | array[object]  | 是     |                
                   |              | LLM 实例配置。 |
-| instances.name                     | string         | 是     |                
                   |              | LLM 服务实例的名称。 |
+| instances.name                     | string         | 是     |                
                   |              | LLM 服务实例的名称。该名称在 `instances` 
中必须唯一,因为负载均衡、健康检查以及 `ai-rate-limiting` 等引用该实例的插件都以它作为实例标识。 |
 | instances.provider                 | string         | 是     |                
                   | [openai, deepseek, azure-openai, aimlapi, anthropic, 
openrouter, gemini, vertex-ai, bedrock, openai-compatible] | LLM 服务提供商。设置为 
`openai` 时,插件将代理请求到 `api.openai.com`。设置为 `deepseek` 时,插件将代理请求到 
`api.deepseek.com`。设置为 `aimlapi` 时,插件使用 OpenAI 兼容驱动程序,默认将请求代理到 
`api.aimlapi.com`。设置为 `anthropic` 时,插件使用 OpenAI 兼容驱动程序,默认将请求代理到 
`api.anthropic.com`。设置为 `openrouter` 时,插件使用 OpenAI 兼容驱动程序,默认将请求代理到 `openro [...]
 | instances.provider_conf            | object         | 否     |                
                   |              | 特定提供商的配置。当 `provider` 设置为 `vertex-ai` 且未配置 
`override` 时必填。当 `provider` 设置为 `bedrock` 时必填。 |
 | instances.provider_conf.project_id | string         | 是     |                
                   |              | Google Cloud 项目 ID。 |
diff --git a/t/plugin/ai-proxy-multi.t b/t/plugin/ai-proxy-multi.t
index c2ecdcd4ff..c676b8fbc9 100644
--- a/t/plugin/ai-proxy-multi.t
+++ b/t/plugin/ai-proxy-multi.t
@@ -570,3 +570,120 @@ qr/6data: \[DONE\]\n\n/
 --- error_code: 400
 --- response_body eval
 qr/.invalid endpoint.*/
+
+
+
+=== TEST 16: reject instances that share a name
+--- 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": "/anything",
+                    "plugins": {
+                        "ai-proxy-multi": {
+                            "instances": [
+                                {
+                                    "name": "same-name",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token-1"
+                                        }
+                                    }
+                                },
+                                {
+                                    "name": "other",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token-2"
+                                        }
+                                    }
+                                },
+                                {
+                                    "name": "same-name",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token-3"
+                                        }
+                                    }
+                                }
+                            ],
+                            "ssl_verify": false
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- error_code: 400
+--- response_body eval
+qr/duplicate instance name 'same-name' at instances\[1\] and instances\[3\]/
+
+
+
+=== TEST 17: instances with distinct names are accepted
+--- 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": "/anything",
+                    "plugins": {
+                        "ai-proxy-multi": {
+                            "instances": [
+                                {
+                                    "name": "key-1",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token-1"
+                                        }
+                                    },
+                                    "override": {
+                                        "endpoint": "http://127.0.0.1:6724";
+                                    }
+                                },
+                                {
+                                    "name": "key-2",
+                                    "provider": "openai",
+                                    "weight": 1,
+                                    "auth": {
+                                        "header": {
+                                            "Authorization": "Bearer token-2"
+                                        }
+                                    },
+                                    "override": {
+                                        "endpoint": "http://127.0.0.1:6724";
+                                    }
+                                }
+                            ],
+                            "ssl_verify": false
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed

Reply via email to