Skip to content

Commit d069213

Browse files
authored
fix(ai-proxy-multi): reject instances that share a name (#13851)
1 parent 5e3ad1f commit d069213

5 files changed

Lines changed: 139 additions & 4 deletions

File tree

apisix/plugins/ai-proxy-multi.lua

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ local ipairs = ipairs
3939
local type = type
4040
local string = string
4141
local sub = string.sub
42+
local str_format = string.format
4243
local url = require("socket.url")
4344

4445
local priority_balancer = require("apisix.balancer.priority")
@@ -124,7 +125,21 @@ function _M.check_schema(conf)
124125
return false, err
125126
end
126127

127-
for _, instance in ipairs(conf.instances) do
128+
-- `instance.name` is the only identifier the runtime has: it keys the
129+
-- balancer nodes, the health checker and the health status, none of which
130+
-- include the priority. Two instances sharing a name therefore collapse
131+
-- into a single logical node with a single checker, so one instance's
132+
-- health decides the other's fate while requests keep using the first
133+
-- instance's auth. Reject duplicates instead of routing on an ambiguity.
134+
local seen_names = core.table.new(0, #conf.instances)
135+
for i, instance in ipairs(conf.instances) do
136+
if seen_names[instance.name] then
137+
return false, str_format("duplicate instance name '%s' at " ..
138+
"instances[%d] and instances[%d]",
139+
instance.name, seen_names[instance.name], i)
140+
end
141+
seen_names[instance.name] = i
142+
128143
local endpoint = instance and instance.override and instance.override.endpoint
129144
if endpoint then
130145
local scheme, host, _ = endpoint:match(endpoint_regex)

apisix/plugins/ai-proxy/schema.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ local ai_instance_schema = {
176176
type = "string",
177177
minLength = 1,
178178
maxLength = 100,
179-
description = "Name of the AI service instance.",
179+
description = "Name of the AI service instance. Must be "
180+
.. "unique within `instances`: it identifies the instance "
181+
.. "in the balancer, in its health checker and in other "
182+
.. "plugins that reference it, such as ai-rate-limiting.",
180183
},
181184
provider = {
182185
type = "string",

docs/en/latest/plugins/ai-proxy-multi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ When an instance's `provider` is set to `bedrock`, the Plugin expects requests i
7575
| 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). |
7676
| 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. |
7777
| instances | array[object] | True | | | LLM instance configurations. |
78-
| instances.name | string | True | | | Name of the LLM service instance. |
78+
| 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`. |
7979
| 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 `api.aimlapi.com` by default. When set to `anthropic`, the Plugin will proxy the request to `api.anthropic.com` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `openrouter.ai` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `generativelanguage.googleapis.com` by default. When set to `vertex-ai`, the Plugin will proxy the request to `aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `bedrock`, the Plugin proxies the request to Amazon Bedrock's Converse API at `bedrock-runtime.{region}.amazonaws.com` and signs the request with AWS SigV4. Requires `provider_conf.region` and `auth.aws`. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |
8080
| 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`. |
8181
| instances.provider_conf.project_id | string | True | | | Google Cloud Project ID. |

docs/zh/latest/plugins/ai-proxy-multi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ import TabItem from '@theme/TabItem';
7575
| 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)组合进行哈希。 |
7676
| balancer.key | string || | |`type``chash` 时使用。当 `hash_on` 设置为 `header``cookie` 时,需要 `key`。当 `hash_on` 设置为 `consumer` 时,不需要 `key`,因为消费者名称将自动用作键。 |
7777
| instances | array[object] || | | LLM 实例配置。 |
78-
| instances.name | string || | | LLM 服务实例的名称。 |
78+
| instances.name | string || | | LLM 服务实例的名称。该名称在 `instances` 中必须唯一,因为负载均衡、健康检查以及 `ai-rate-limiting` 等引用该实例的插件都以它作为实例标识。 |
7979
| 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 兼容驱动程序,默认将请求代理到 `openrouter.ai`。设置为 `gemini` 时,插件使用 OpenAI 兼容驱动程序,默认将请求代理到 `generativelanguage.googleapis.com`。设置为 `vertex-ai` 时,插件默认将请求代理到 `aiplatform.googleapis.com`,且需要配置 `provider_conf` 或 `override`。设置为 `bedrock` 时,插件将代理请求到 AWS Bedrock Converse API(`bedrock-runtime.<region>.amazonaws.com`),并使用 AWS SigV4 对请求进行签名。设置为 `openai-compatible` 时,插件将代理请求到在 `override` 中配置的自定义端点。 |
8080
| instances.provider_conf | object || | | 特定提供商的配置。当 `provider` 设置为 `vertex-ai` 且未配置 `override` 时必填。当 `provider` 设置为 `bedrock` 时必填。 |
8181
| instances.provider_conf.project_id | string || | | Google Cloud 项目 ID。 |

t/plugin/ai-proxy-multi.t

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,120 @@ qr/6data: \[DONE\]\n\n/
570570
--- error_code: 400
571571
--- response_body eval
572572
qr/.invalid endpoint.*/
573+
574+
575+
576+
=== TEST 16: reject instances that share a name
577+
--- config
578+
location /t {
579+
content_by_lua_block {
580+
local t = require("lib.test_admin").test
581+
local code, body = t('/apisix/admin/routes/1',
582+
ngx.HTTP_PUT,
583+
[[{
584+
"uri": "/anything",
585+
"plugins": {
586+
"ai-proxy-multi": {
587+
"instances": [
588+
{
589+
"name": "same-name",
590+
"provider": "openai",
591+
"weight": 1,
592+
"auth": {
593+
"header": {
594+
"Authorization": "Bearer token-1"
595+
}
596+
}
597+
},
598+
{
599+
"name": "other",
600+
"provider": "openai",
601+
"weight": 1,
602+
"auth": {
603+
"header": {
604+
"Authorization": "Bearer token-2"
605+
}
606+
}
607+
},
608+
{
609+
"name": "same-name",
610+
"provider": "openai",
611+
"weight": 1,
612+
"auth": {
613+
"header": {
614+
"Authorization": "Bearer token-3"
615+
}
616+
}
617+
}
618+
],
619+
"ssl_verify": false
620+
}
621+
}
622+
}]]
623+
)
624+
625+
if code >= 300 then
626+
ngx.status = code
627+
end
628+
ngx.say(body)
629+
}
630+
}
631+
--- error_code: 400
632+
--- response_body eval
633+
qr/duplicate instance name 'same-name' at instances\[1\] and instances\[3\]/
634+
635+
636+
637+
=== TEST 17: instances with distinct names are accepted
638+
--- config
639+
location /t {
640+
content_by_lua_block {
641+
local t = require("lib.test_admin").test
642+
local code, body = t('/apisix/admin/routes/1',
643+
ngx.HTTP_PUT,
644+
[[{
645+
"uri": "/anything",
646+
"plugins": {
647+
"ai-proxy-multi": {
648+
"instances": [
649+
{
650+
"name": "key-1",
651+
"provider": "openai",
652+
"weight": 1,
653+
"auth": {
654+
"header": {
655+
"Authorization": "Bearer token-1"
656+
}
657+
},
658+
"override": {
659+
"endpoint": "http://127.0.0.1:6724"
660+
}
661+
},
662+
{
663+
"name": "key-2",
664+
"provider": "openai",
665+
"weight": 1,
666+
"auth": {
667+
"header": {
668+
"Authorization": "Bearer token-2"
669+
}
670+
},
671+
"override": {
672+
"endpoint": "http://127.0.0.1:6724"
673+
}
674+
}
675+
],
676+
"ssl_verify": false
677+
}
678+
}
679+
}]]
680+
)
681+
682+
if code >= 300 then
683+
ngx.status = code
684+
end
685+
ngx.say(body)
686+
}
687+
}
688+
--- response_body
689+
passed

0 commit comments

Comments
 (0)