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