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

shreemaan-abhishek 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 2b6df4f3b refactor(ai-aws-content-moderation): moderate decoded LLM 
content in access phase (#13647)
2b6df4f3b is described below

commit 2b6df4f3b31c2f3298b9c78719c17094a29dad02
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Tue Jul 7 15:29:03 2026 +0800

    refactor(ai-aws-content-moderation): moderate decoded LLM content in access 
phase (#13647)
---
 apisix/plugins/ai-aws-content-moderation.lua       | 167 +++++++++++++++------
 .../en/latest/plugins/ai-aws-content-moderation.md |  61 ++++++--
 .../zh/latest/plugins/ai-aws-content-moderation.md |  61 ++++++--
 t/admin/plugins.t                                  |   2 +-
 t/plugin/ai-aws-content-moderation-secrets.t       |  49 +++---
 t/plugin/ai-aws-content-moderation.t               | 156 +++++++++----------
 t/plugin/ai-aws-content-moderation2.t              |  26 ++--
 7 files changed, 337 insertions(+), 185 deletions(-)

diff --git a/apisix/plugins/ai-aws-content-moderation.lua 
b/apisix/plugins/ai-aws-content-moderation.lua
index a3a1295c6..93ed36857 100644
--- a/apisix/plugins/ai-aws-content-moderation.lua
+++ b/apisix/plugins/ai-aws-content-moderation.lua
@@ -16,17 +16,20 @@
 --
 require("resty.aws.config") -- to read env vars before initing aws module
 
-local core = require("apisix.core")
-local binding = require("apisix.plugins.ai-protocols.binding")
-local aws = require("resty.aws")
+local core      = require("apisix.core")
+local protocols = require("apisix.plugins.ai-protocols")
+local binding   = require("apisix.plugins.ai-protocols.binding")
+local aws       = require("resty.aws")
 local aws_instance
 
 local http = require("resty.http")
 
-local pairs = pairs
-local unpack = unpack
-local type = type
-local ipairs = ipairs
+local ngx     = ngx
+local pairs   = pairs
+local unpack  = unpack
+local type    = type
+local ipairs  = ipairs
+local table   = table
 local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR
 local HTTP_BAD_REQUEST = ngx.HTTP_BAD_REQUEST
 
@@ -69,6 +72,9 @@ local schema = {
             maximum = 1,
             default = 0.5
         },
+        check_request = { type = "boolean", default = true },
+        deny_code = { type = "number", default = 200 },
+        deny_message = { type = "string" },
         fail_mode = binding.schema_property("skip"),
     },
     encrypt_fields = { "comprehend.secret_access_key" },
@@ -78,7 +84,7 @@ local schema = {
 
 local _M = {
     version  = 0.1,
-    priority = 1050,
+    priority = 1031,
     name     = "ai-aws-content-moderation",
     schema   = schema,
 }
@@ -89,29 +95,10 @@ function _M.check_schema(conf)
 end
 
 
-function _M.rewrite(conf, ctx)
-    -- Consumer-bound moderation may receive non-AI traffic (e.g. 
multipart/binary
-    -- uploads) whose body can't be moderated as text. Govern that via 
fail_mode.
-    local ct = core.request.header(ctx, "Content-Type")
-    -- media types are case-insensitive, normalize before matching
-    ct = ct and ct:lower()
-    if ct and not core.string.has_prefix(ct, "application/json") then
-        local handled, code, resp = binding.on_unsupported(
-            conf.fail_mode, _M.name, ctx,
-            "unsupported content-type: " .. ct,
-            HTTP_BAD_REQUEST, "unsupported content-type: " .. ct
-                .. ", only application/json is supported")
-        if handled then
-            return code, resp
-        end
-        return
-    end
-
-    local body, err = core.request.get_body()
-    if not body then
-        return HTTP_BAD_REQUEST, err
-    end
-
+-- Score content with AWS Comprehend detectToxicContent.
+-- Returns (reason, nil) when a category/toxicity threshold is exceeded,
+-- (nil, err) on a service error, and (nil, nil) when the content is clean.
+local function detect_toxic(conf, content)
     local comprehend = conf.comprehend
 
     if not aws_instance then
@@ -129,46 +116,136 @@ function _M.rewrite(conf, ctx)
     aws_instance.config.endpoint = endpoint
     aws_instance.config.ssl_verify = comprehend.ssl_verify
 
-    local comprehend = aws_instance:Comprehend({
+    local comprehend_client = aws_instance:Comprehend({
         credentials = credentials,
         endpoint = endpoint,
         region = comprehend.region,
         port = port,
     })
 
-    local res, err = comprehend:detectToxicContent({
+    local res, err = comprehend_client:detectToxicContent({
         LanguageCode = "en",
         TextSegments = {{
-            Text = body
+            Text = content
         }},
     })
-
     if not res then
-        core.log.error("failed to send request to ", endpoint, ": ", err)
-        return HTTP_INTERNAL_SERVER_ERROR, err
+        return nil, "failed to send request to " .. endpoint .. ": " .. err
     end
 
     local results = res.body and res.body.ResultList
     if type(results) ~= "table" or core.table.isempty(results) then
-        return HTTP_INTERNAL_SERVER_ERROR, "failed to get moderation results 
from response"
+        return nil, "failed to get moderation results from response"
     end
 
     for _, result in ipairs(results) do
         if conf.moderation_categories then
             for _, item in pairs(result.Labels) do
-                if not conf.moderation_categories[item.Name] then
-                    goto continue
-                end
-                if item.Score > conf.moderation_categories[item.Name] then
-                    return HTTP_BAD_REQUEST, "request body exceeds " .. 
item.Name .. " threshold"
+                local threshold = conf.moderation_categories[item.Name]
+                if threshold and item.Score > threshold then
+                    return "request body exceeds " .. item.Name .. " threshold"
                 end
-                ::continue::
             end
         end
 
         if result.Toxicity > conf.moderation_threshold then
-            return HTTP_BAD_REQUEST, "request body exceeds toxicity threshold"
+            return "request body exceeds toxicity threshold"
+        end
+    end
+end
+
+
+-- Build a provider-compatible deny body so the AI client isn't broken.
+local function build_deny_message(ctx, conf, reason)
+    local message = conf.deny_message or reason
+    local proto = protocols.get(ctx.ai_client_protocol)
+    if not proto then
+        return message
+    end
+    local stream = ctx.var.request_type == "ai_stream"
+    local usage = ctx.llm_raw_usage
+        or (proto.empty_usage and proto.empty_usage())
+        or { prompt_tokens = 0, completion_tokens = 0, total_tokens = 0 }
+    return proto.build_deny_response({
+        text = message,
+        model = ctx.var.request_llm_model,
+        usage = usage,
+        stream = stream,
+    })
+end
+
+
+function _M.access(conf, ctx)
+    if not ctx.picked_ai_instance then
+        local handled, code, body = binding.on_unsupported(
+            conf.fail_mode, _M.name, ctx,
+            "no ai instance picked (request did not pass through 
ai-proxy/ai-proxy-multi)",
+            HTTP_INTERNAL_SERVER_ERROR, "no ai instance picked, " ..
+                "ai-aws-content-moderation plugin must be used with " ..
+                "ai-proxy or ai-proxy-multi plugin")
+        if handled then
+            return code, body
+        end
+        return
+    end
+
+    if not conf.check_request then
+        core.log.info("skip request check for this request")
+        return
+    end
+
+    local ct = core.request.header(ctx, "Content-Type")
+    -- media types are case-insensitive, normalize before matching
+    ct = ct and ct:lower()
+    if ct and not core.string.has_prefix(ct, "application/json") then
+        local handled, code, body = binding.on_unsupported(
+            conf.fail_mode, _M.name, ctx,
+            "unsupported content-type: " .. ct,
+            HTTP_BAD_REQUEST, "unsupported content-type: " .. ct
+                .. ", only application/json is supported")
+        if handled then
+            return code, body
+        end
+        return
+    end
+
+    local request_tab, err = core.request.get_json_request_body_table()
+    if not request_tab then
+        return HTTP_BAD_REQUEST, err
+    end
+
+    local proto = protocols.get(ctx.ai_client_protocol)
+    if not proto or not proto.extract_request_content then
+        local handled, code, body = binding.on_unsupported(
+            conf.fail_mode, _M.name, ctx,
+            "unsupported protocol: " .. (ctx.ai_client_protocol or "unknown"),
+            HTTP_INTERNAL_SERVER_ERROR,
+            "unsupported protocol: " .. (ctx.ai_client_protocol or "unknown"))
+        if handled then
+            return code, body
+        end
+        return
+    end
+
+    local contents = proto.extract_request_content(request_tab)
+    local content = table.concat(contents, " ")
+    if content == "" then
+        return
+    end
+
+    local reason, err = detect_toxic(conf, content)
+    if err then
+        core.log.error(err)
+        return HTTP_INTERNAL_SERVER_ERROR, err
+    end
+    if reason then
+        local stream = ctx.var.request_type == "ai_stream"
+        if stream then
+            core.response.set_header("Content-Type", "text/event-stream")
+        else
+            core.response.set_header("Content-Type", "application/json")
         end
+        return conf.deny_code, build_deny_message(ctx, conf, reason)
     end
 end
 
diff --git a/docs/en/latest/plugins/ai-aws-content-moderation.md 
b/docs/en/latest/plugins/ai-aws-content-moderation.md
index fce2755a3..2199d9083 100644
--- a/docs/en/latest/plugins/ai-aws-content-moderation.md
+++ b/docs/en/latest/plugins/ai-aws-content-moderation.md
@@ -36,9 +36,11 @@ import TabItem from '@theme/TabItem';
 
 ## Description
 
-The `ai-aws-content-moderation` Plugin integrates with [AWS 
Comprehend](https://aws.amazon.com/comprehend/) to check request bodies for 
toxicity when proxying to LLMs, such as profanity, hate speech, insult, 
harassment, violence, and more, rejecting requests if the evaluated outcome 
exceeds the configured threshold.
+The `ai-aws-content-moderation` Plugin integrates with [AWS 
Comprehend](https://aws.amazon.com/comprehend/) to check request content for 
toxicity when proxying to LLMs, such as profanity, hate speech, insult, 
harassment, violence, and more, rejecting requests if the evaluated outcome 
exceeds the configured threshold.
 
-This Plugin must be used in Routes that proxy requests to LLMs only.
+The Plugin is protocol-aware: it extracts the prompt content from the LLM 
request (for example `messages[].content`) and moderates only that decoded 
text, rather than the raw request body.
+
+The `ai-aws-content-moderation` Plugin should be used with either 
[`ai-proxy`](./ai-proxy.md) or [`ai-proxy-multi`](./ai-proxy-multi.md) Plugin 
for proxying LLM requests.
 
 ## Plugin Attributes
 
@@ -52,7 +54,10 @@ This Plugin must be used in Routes that proxy requests to 
LLMs only.
 | `comprehend.ssl_verify` | boolean | False | true | | If true, enable TLS 
certificate verification. |
 | `moderation_categories` | object | False | | | Key-value pairs of moderation 
category and their corresponding threshold. In each pair, the key should be one 
of `PROFANITY`, `HATE_SPEECH`, `INSULT`, `HARASSMENT_OR_ABUSE`, `SEXUAL`, or 
`VIOLENCE_OR_THREAT`; and the threshold value should be between 0 and 1 
(inclusive). |
 | `moderation_threshold` | number | False | 0.5 | 0 - 1 | Overall toxicity 
threshold. A higher value means more toxic content allowed. This option differs 
from the individual category thresholds in `moderation_categories`. For 
example, if `moderation_categories` is set with a `PROFANITY` threshold of 
`0.5`, and a request has a `PROFANITY` score of `0.1`, the request will not 
exceed the category threshold. However, if the request has other categories 
like `SEXUAL` or `VIOLENCE_OR_THREAT`  [...]
-| `fail_mode` | string | False | `skip` | `skip`, `warn`, `error` | Behavior 
when the request body is not a recognized AI request that this plugin can 
inspect (for example, a non-JSON `multipart/form-data` upload on a 
Consumer-bound plugin, or a request that did not pass through `ai-proxy`). 
`skip`: let the request pass through unchecked; `warn`: pass through and log a 
warning; `error`: reject the request. |
+| `check_request` | boolean | False | `true` | | If `true`, moderate the 
request content. |
+| `deny_code` | number | False | `200` | | HTTP status code returned when a 
request is rejected. |
+| `deny_message` | string | False | | | Message returned when a request is 
rejected. If unset, the moderation reason (for example `request body exceeds 
toxicity threshold`) is returned. |
+| `fail_mode` | string | False | `skip` | `skip`, `warn`, `error` | Behavior 
when the request did not pass through `ai-proxy`/`ai-proxy-multi` and therefore 
cannot be moderated as an AI request. `skip`: let the request pass through 
unchecked; `warn`: pass through and log a warning; `error`: reject the request. 
|
 
 ## Examples
 
@@ -103,7 +108,8 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1"; -X PUT \
         },
         "moderation_categories": {
           "PROFANITY": 0.1
-        }
+        },
+        "deny_code": 400
       },
       "ai-proxy": {
         "provider": "openai",
@@ -142,6 +148,7 @@ services:
               region: us-east-1
             moderation_categories:
               PROFANITY: 0.1
+            deny_code: 400
           ai-proxy:
             provider: openai
             auth:
@@ -181,6 +188,7 @@ spec:
           region: us-east-1
         moderation_categories:
           PROFANITY: 0.1
+        deny_code: 400
     - name: ai-proxy
       config:
         provider: openai
@@ -242,6 +250,7 @@ spec:
               region: us-east-1
             moderation_categories:
               PROFANITY: 0.1
+            deny_code: 400
         - name: ai-proxy
           enable: true
           config:
@@ -278,10 +287,23 @@ curl -i "http://127.0.0.1:9080/post"; -X POST \
   }'
 ```
 
-You should receive an `HTTP/1.1 400 Bad Request` response and see the 
following message:
+You should receive an `HTTP/1.1 400 Bad Request` response. The moderation 
reason is returned in the response body in a provider-compatible format, so AI 
clients are not broken:
 
-```text
-request body exceeds PROFANITY threshold
+```json
+{
+  ...,
+  "choices": [
+    {
+      "index": 0,
+      "message": {
+        "role": "assistant",
+        "content": "request body exceeds PROFANITY threshold"
+      },
+      "finish_reason": "stop"
+    }
+  ],
+  ...
+}
 ```
 
 Send another request to the Route with a typical question in the request body:
@@ -343,7 +365,8 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1"; -X PUT \
         "moderation_categories": {
           "PROFANITY": 1
         },
-        "moderation_threshold": 0.2
+        "moderation_threshold": 0.2,
+        "deny_code": 400
       },
       "ai-proxy": {
         "provider": "openai",
@@ -383,6 +406,7 @@ services:
             moderation_categories:
               PROFANITY: 1
             moderation_threshold: 0.2
+            deny_code: 400
           ai-proxy:
             provider: openai
             auth:
@@ -423,6 +447,7 @@ spec:
         moderation_categories:
           PROFANITY: 1
         moderation_threshold: 0.2
+        deny_code: 400
     - name: ai-proxy
       config:
         provider: openai
@@ -485,6 +510,7 @@ spec:
             moderation_categories:
               PROFANITY: 1
             moderation_threshold: 0.2
+            deny_code: 400
         - name: ai-proxy
           enable: true
           config:
@@ -521,10 +547,23 @@ curl -i "http://127.0.0.1:9080/post"; -X POST \
   }'
 ```
 
-You should receive an `HTTP/1.1 400 Bad Request` response and see the 
following message:
+You should receive an `HTTP/1.1 400 Bad Request` response. The moderation 
reason is returned in the response body in a provider-compatible format, so AI 
clients are not broken:
 
-```text
-request body exceeds toxicity threshold
+```json
+{
+  ...,
+  "choices": [
+    {
+      "index": 0,
+      "message": {
+        "role": "assistant",
+        "content": "request body exceeds toxicity threshold"
+      },
+      "finish_reason": "stop"
+    }
+  ],
+  ...
+}
 ```
 
 Send another request to the Route without any profane word in the request body:
diff --git a/docs/zh/latest/plugins/ai-aws-content-moderation.md 
b/docs/zh/latest/plugins/ai-aws-content-moderation.md
index f1dae3296..c002b3f48 100644
--- a/docs/zh/latest/plugins/ai-aws-content-moderation.md
+++ b/docs/zh/latest/plugins/ai-aws-content-moderation.md
@@ -38,9 +38,11 @@ import TabItem from '@theme/TabItem';
 
 ## 描述
 
-`ai-aws-content-moderation` 插件集成了 [AWS 
Comprehend](https://aws.amazon.com/comprehend/),用于在代理请求到 LLM 
时检查请求体中的有害内容,例如亵渎、仇恨言论、侮辱、骚扰、暴力等,如果评估结果超过配置的阈值则拒绝请求。
+`ai-aws-content-moderation` 插件集成了 [AWS 
Comprehend](https://aws.amazon.com/comprehend/),用于在代理请求到 LLM 
时检查请求内容中的有害内容,例如亵渎、仇恨言论、侮辱、骚扰、暴力等,如果评估结果超过配置的阈值则拒绝请求。
 
-此插件只能在代理请求到 LLM 的路由中使用。
+该插件是协议感知的:它会从 LLM 请求中提取提示内容(例如 `messages[].content`),仅审核解码后的文本,而不是原始请求体。
+
+`ai-aws-content-moderation` 插件应与 [`ai-proxy`](./ai-proxy.md) 或 
[`ai-proxy-multi`](./ai-proxy-multi.md) 插件一起使用,以代理 LLM 请求。
 
 ## 插件属性
 
@@ -54,7 +56,10 @@ import TabItem from '@theme/TabItem';
 | `comprehend.ssl_verify` | boolean | 否 | true | | 如果为 true,则启用 TLS 证书验证。 |
 | `moderation_categories` | object | 否 | | | 审核类别及其对应阈值的键值对。在每个键值对中,键应为 
`PROFANITY`、`HATE_SPEECH`、`INSULT`、`HARASSMENT_OR_ABUSE`、`SEXUAL` 或 
`VIOLENCE_OR_THREAT` 之一;阈值应在 0 到 1 之间(包含)。 |
 | `moderation_threshold` | number | 否 | 0.5 | 0 - 1 | 
整体毒性阈值。值越高,允许的有害内容越多。此选项与 `moderation_categories` 中的单独类别阈值不同。例如,如果 
`moderation_categories` 中设置了 `PROFANITY` 阈值为 `0.5`,而请求的 `PROFANITY` 分数为 
`0.1`,则请求不会超过类别阈值。但如果请求的其他类别(如 `SEXUAL` 或 `VIOLENCE_OR_THREAT`)超过了 
`moderation_threshold`,则请求将被拒绝。 |
-| `fail_mode` | string | 否 | `skip` | `skip`、`warn`、`error` | 当请求体不是该插件可识别的 AI 
请求时的处理行为(例如 Consumer 级别绑定时的非 JSON `multipart/form-data` 上传,或未经过 `ai-proxy` 
的请求)。`skip`:放行请求且不做检查;`warn`:放行并记录 warning 日志;`error`:拒绝请求。 |
+| `check_request` | boolean | 否 | `true` | | 如果为 `true`,则审核请求内容。 |
+| `deny_code` | number | 否 | `200` | | 请求被拒绝时返回的 HTTP 状态码。 |
+| `deny_message` | string | 否 | | | 请求被拒绝时返回的消息。未设置时,返回审核原因(例如 `request body 
exceeds toxicity threshold`)。 |
+| `fail_mode` | string | 否 | `skip` | `skip`、`warn`、`error` | 当请求未经过 
`ai-proxy`/`ai-proxy-multi`,因而无法作为 AI 
请求进行审核时的处理行为。`skip`:放行请求且不做检查;`warn`:放行并记录 warning 日志;`error`:拒绝请求。 |
 
 ## 使用示例
 
@@ -105,7 +110,8 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1"; -X PUT \
         },
         "moderation_categories": {
           "PROFANITY": 0.1
-        }
+        },
+        "deny_code": 400
       },
       "ai-proxy": {
         "provider": "openai",
@@ -144,6 +150,7 @@ services:
               region: us-east-1
             moderation_categories:
               PROFANITY: 0.1
+            deny_code: 400
           ai-proxy:
             provider: openai
             auth:
@@ -183,6 +190,7 @@ spec:
           region: us-east-1
         moderation_categories:
           PROFANITY: 0.1
+        deny_code: 400
     - name: ai-proxy
       config:
         provider: openai
@@ -244,6 +252,7 @@ spec:
               region: us-east-1
             moderation_categories:
               PROFANITY: 0.1
+            deny_code: 400
         - name: ai-proxy
           enable: true
           config:
@@ -280,10 +289,23 @@ curl -i "http://127.0.0.1:9080/post"; -X POST \
   }'
 ```
 
-您应该收到 `HTTP/1.1 400 Bad Request` 响应,并看到以下消息:
+您应该收到 `HTTP/1.1 400 Bad Request` 响应。审核原因会以与提供商兼容的格式返回在响应体中,从而不会破坏 AI 客户端:
 
-```text
-request body exceeds PROFANITY threshold
+```json
+{
+  ...,
+  "choices": [
+    {
+      "index": 0,
+      "message": {
+        "role": "assistant",
+        "content": "request body exceeds PROFANITY threshold"
+      },
+      "finish_reason": "stop"
+    }
+  ],
+  ...
+}
 ```
 
 向路由发送另一个包含正常问题的请求:
@@ -345,7 +367,8 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1"; -X PUT \
         "moderation_categories": {
           "PROFANITY": 1
         },
-        "moderation_threshold": 0.2
+        "moderation_threshold": 0.2,
+        "deny_code": 400
       },
       "ai-proxy": {
         "provider": "openai",
@@ -385,6 +408,7 @@ services:
             moderation_categories:
               PROFANITY: 1
             moderation_threshold: 0.2
+            deny_code: 400
           ai-proxy:
             provider: openai
             auth:
@@ -425,6 +449,7 @@ spec:
         moderation_categories:
           PROFANITY: 1
         moderation_threshold: 0.2
+        deny_code: 400
     - name: ai-proxy
       config:
         provider: openai
@@ -487,6 +512,7 @@ spec:
             moderation_categories:
               PROFANITY: 1
             moderation_threshold: 0.2
+            deny_code: 400
         - name: ai-proxy
           enable: true
           config:
@@ -523,10 +549,23 @@ curl -i "http://127.0.0.1:9080/post"; -X POST \
   }'
 ```
 
-您应该收到 `HTTP/1.1 400 Bad Request` 响应,并看到以下消息:
+您应该收到 `HTTP/1.1 400 Bad Request` 响应。审核原因会以与提供商兼容的格式返回在响应体中,从而不会破坏 AI 客户端:
 
-```text
-request body exceeds toxicity threshold
+```json
+{
+  ...,
+  "choices": [
+    {
+      "index": 0,
+      "message": {
+        "role": "assistant",
+        "content": "request body exceeds toxicity threshold"
+      },
+      "finish_reason": "stop"
+    }
+  ],
+  ...
+}
 ```
 
 向路由发送另一个不含亵渎词汇的请求:
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index ec33d940a..f0a512069 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -105,10 +105,10 @@ ai-prompt-guard
 ai-prompt-template
 ai-prompt-decorator
 ai-rag
-ai-aws-content-moderation
 ai-proxy-multi
 ai-proxy
 ai-cache
+ai-aws-content-moderation
 ai-rate-limiting
 ai-aliyun-content-moderation
 ai-lakera-guard
diff --git a/t/plugin/ai-aws-content-moderation-secrets.t 
b/t/plugin/ai-aws-content-moderation-secrets.t
index a88171ac1..5d069e52d 100644
--- a/t/plugin/ai-aws-content-moderation-secrets.t
+++ b/t/plugin/ai-aws-content-moderation-secrets.t
@@ -39,6 +39,9 @@ _EOC_
 
     $block->set_value("main_config", $main_config);
 
+    # Mock AWS Comprehend: only answer (with a clean result) when the SigV4
+    # Authorization header carries the resolved access key id, proving the
+    # secret reference reached the plugin.
     my $http_config = $block->http_config // <<_EOC_;
         server {
             listen 2668;
@@ -88,11 +91,9 @@ __DATA__
 
 === TEST 1: store secret into vault
 --- exec
-VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo 
secret_access_key=super-secret
-VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo 
access_key_id=access-key-id
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/foo 
secret_access_key=super-secret access_key_id=access-key-id
 --- response_body
 Success! Data written to: kv/apisix/foo
-Success! Data written to: kv/apisix/foo
 
 
 
@@ -118,8 +119,13 @@ Success! Data written to: kv/apisix/foo
             local code, body = t('/apisix/admin/routes/1',
                  ngx.HTTP_PUT,
                  [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": 
"$secret://vault/test1/foo/access_key_id",
@@ -128,12 +134,6 @@ Success! Data written to: kv/apisix/foo
                                 "endpoint": "http://localhost:2668";
                             }
                         }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
-                        }
                     }
                 }]]
             )
@@ -152,13 +152,13 @@ success
 
 
 
-=== TEST 3: good request should pass
+=== TEST 3: good request should pass (secret credentials reach Comprehend)
 --- request
-POST /echo
+POST /chat
 {"model":"gpt-4o-mini","messages":[{"role":"user","content":"good_request"}]}
 --- error_code: 200
---- response_body chomp
-{"model":"gpt-4o-mini","messages":[{"role":"user","content":"good_request"}]}
+--- response_body_like eval
+qr/good_request/
 
 
 
@@ -170,8 +170,13 @@ POST /echo
             local code, body = t('/apisix/admin/routes/1',
                  ngx.HTTP_PUT,
                  [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": "$env://ACCESS_KEY_ID",
@@ -180,12 +185,6 @@ POST /echo
                                 "endpoint": "http://localhost:2668";
                             }
                         }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
-                        }
                     }
                 }]]
             )
@@ -204,10 +203,10 @@ success
 
 
 
-=== TEST 5: good request should pass
+=== TEST 5: good request should pass (env credentials reach Comprehend)
 --- request
-POST /echo
+POST /chat
 {"model":"gpt-4o-mini","messages":[{"role":"user","content":"good_request"}]}
 --- error_code: 200
---- response_body chomp
-{"model":"gpt-4o-mini","messages":[{"role":"user","content":"good_request"}]}
+--- response_body_like eval
+qr/good_request/
diff --git a/t/plugin/ai-aws-content-moderation.t 
b/t/plugin/ai-aws-content-moderation.t
index 765bba1ab..70baeb501 100644
--- a/t/plugin/ai-aws-content-moderation.t
+++ b/t/plugin/ai-aws-content-moderation.t
@@ -36,6 +36,8 @@ _EOC_
 
     $block->set_value("main_config", $main_config);
 
+    # Mock AWS Comprehend detectToxicContent: looks up the extracted text
+    # (TextSegments[1].Text) in the canned responses fixture.
     my $http_config = $block->http_config // <<_EOC_;
         server {
             listen 2668;
@@ -97,7 +99,7 @@ run_tests();
 
 __DATA__
 
-=== TEST 1: sanity
+=== TEST 1: sanity, ai-proxy + moderation
 --- config
     location /t {
         content_by_lua_block {
@@ -105,21 +107,21 @@ __DATA__
             local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PUT,
                 [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": "access",
                                 "secret_access_key": "ea+secret",
                                 "region": "us-east-1",
                                 "endpoint": "http://localhost:2668";
-                            }
-                        }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
+                            },
+                            "deny_code": 400
                         }
                     }
                 }]]
@@ -138,19 +140,21 @@ passed
 
 === TEST 2: toxic request should fail
 --- request
-POST /echo
-toxic
+POST /chat
+{ "messages": [ { "role": "user", "content": "toxic" } ] }
 --- error_code: 400
---- response_body chomp
-request body exceeds toxicity threshold
+--- response_body_like eval
+qr/request body exceeds toxicity threshold/
 
 
 
 === TEST 3: good request should pass
 --- request
-POST /echo
-good_request
+POST /chat
+{ "messages": [ { "role": "user", "content": "good_request" } ] }
 --- error_code: 200
+--- response_body_like eval
+qr/good_request/
 
 
 
@@ -162,8 +166,13 @@ good_request
             local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PUT,
                 [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": "access",
@@ -171,15 +180,8 @@ good_request
                                 "region": "us-east-1",
                                 "endpoint": "http://localhost:2668";
                             },
-                            "moderation_categories": {
-                                "PROFANITY": 0.5
-                            }
-                        }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
+                            "moderation_categories": { "PROFANITY": 0.5 },
+                            "deny_code": 400
                         }
                     }
                 }]]
@@ -198,29 +200,31 @@ passed
 
 === TEST 5: profane request should fail
 --- request
-POST /echo
-profane
+POST /chat
+{ "messages": [ { "role": "user", "content": "profane" } ] }
 --- error_code: 400
---- response_body chomp
-request body exceeds PROFANITY threshold
+--- response_body_like eval
+qr/request body exceeds PROFANITY threshold/
 
 
 
 === TEST 6: very profane request should also fail
 --- request
-POST /echo
-very_profane
+POST /chat
+{ "messages": [ { "role": "user", "content": "very_profane" } ] }
 --- error_code: 400
---- response_body chomp
-request body exceeds PROFANITY threshold
+--- response_body_like eval
+qr/request body exceeds PROFANITY threshold/
 
 
 
 === TEST 7: good_request should pass
 --- request
-POST /echo
-good_request
+POST /chat
+{ "messages": [ { "role": "user", "content": "good_request" } ] }
 --- error_code: 200
+--- response_body_like eval
+qr/good_request/
 
 
 
@@ -232,8 +236,13 @@ good_request
             local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PUT,
                 [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": "access",
@@ -241,15 +250,8 @@ good_request
                                 "region": "us-east-1",
                                 "endpoint": "http://localhost:2668";
                             },
-                            "moderation_categories": {
-                                "PROFANITY": 0.7
-                            }
-                        }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
+                            "moderation_categories": { "PROFANITY": 0.7 },
+                            "deny_code": 400
                         }
                     }
                 }]]
@@ -268,41 +270,45 @@ passed
 
 === TEST 9: profane request should pass profanity check but fail toxicity check
 --- request
-POST /echo
-profane
+POST /chat
+{ "messages": [ { "role": "user", "content": "profane" } ] }
 --- error_code: 400
---- response_body chomp
-request body exceeds toxicity threshold
+--- response_body_like eval
+qr/request body exceeds toxicity threshold/
 
 
 
 === TEST 10: profane_but_not_toxic request should pass
 --- request
-POST /echo
-profane_but_not_toxic
+POST /chat
+{ "messages": [ { "role": "user", "content": "profane_but_not_toxic" } ] }
 --- error_code: 200
+--- response_body_like eval
+qr/profane_but_not_toxic/
 
 
 
 === TEST 11: but very profane request will fail
 --- request
-POST /echo
-very_profane
+POST /chat
+{ "messages": [ { "role": "user", "content": "very_profane" } ] }
 --- error_code: 400
---- response_body chomp
-request body exceeds PROFANITY threshold
+--- response_body_like eval
+qr/request body exceeds PROFANITY threshold/
 
 
 
 === TEST 12: good_request should pass
 --- request
-POST /echo
-good_request
+POST /chat
+{ "messages": [ { "role": "user", "content": "good_request" } ] }
 --- error_code: 200
+--- response_body_like eval
+qr/good_request/
 
 
 
-=== TEST 13: setup route with default fail_mode (skip)
+=== TEST 13: setup route without ai-proxy, default fail_mode (skip)
 --- config
     location /t {
         content_by_lua_block {
@@ -323,9 +329,7 @@ good_request
                     },
                     "upstream": {
                         "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
-                        }
+                        "nodes": { "127.0.0.1:1980": 1 }
                     }
                 }]]
             )
@@ -341,19 +345,19 @@ passed
 
 
 
-=== TEST 14: non-JSON (multipart) request passes through by default (skip) to 
upstream
+=== TEST 14: request without ai-proxy passes through unchecked (skip)
 --- request
 POST /echo
-name=alice&action=upload
---- more_headers
-Content-Type: multipart/form-data
+{ "messages": [ { "role": "user", "content": "toxic" } ] }
 --- error_code: 200
---- response_body chomp
-name=alice&action=upload
+--- response_body_like eval
+qr/toxic/
+--- error_log
+ai-aws-content-moderation skipped: no ai instance picked
 
 
 
-=== TEST 15: setup route with fail_mode=error
+=== TEST 15: setup route without ai-proxy, fail_mode=error
 --- config
     location /t {
         content_by_lua_block {
@@ -375,9 +379,7 @@ name=alice&action=upload
                     },
                     "upstream": {
                         "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
-                        }
+                        "nodes": { "127.0.0.1:1980": 1 }
                     }
                 }]]
             )
@@ -393,12 +395,10 @@ passed
 
 
 
-=== TEST 16: non-JSON request is rejected when fail_mode=error
+=== TEST 16: request without ai-proxy is rejected when fail_mode=error
 --- request
 POST /echo
-name=alice&action=upload
---- more_headers
-Content-Type: multipart/form-data
---- error_code: 400
---- response_body eval
-qr/only application\/json is supported/
+{ "messages": [ { "role": "user", "content": "good_request" } ] }
+--- error_code: 500
+--- response_body_chomp
+no ai instance picked, ai-aws-content-moderation plugin must be used with 
ai-proxy or ai-proxy-multi plugin
diff --git a/t/plugin/ai-aws-content-moderation2.t 
b/t/plugin/ai-aws-content-moderation2.t
index 869fcf09d..364077701 100644
--- a/t/plugin/ai-aws-content-moderation2.t
+++ b/t/plugin/ai-aws-content-moderation2.t
@@ -41,7 +41,7 @@ run_tests();
 
 __DATA__
 
-=== TEST 1: sanity
+=== TEST 1: sanity, ai-proxy + moderation (no Comprehend server listening)
 --- config
     location /t {
         content_by_lua_block {
@@ -49,22 +49,20 @@ __DATA__
             local code, body = t('/apisix/admin/routes/1',
                 ngx.HTTP_PUT,
                 [[{
-                    "uri": "/echo",
+                    "uri": "/chat",
                     "plugins": {
+                        "ai-proxy": {
+                            "provider": "openai",
+                            "auth": { "header": { "Authorization": "Bearer 
token" } },
+                            "override": { "endpoint": 
"http://127.0.0.1:1980/v1/chat/completions"; }
+                        },
                         "ai-aws-content-moderation": {
                             "comprehend": {
                                 "access_key_id": "access",
                                 "secret_access_key": "ea+secret",
                                 "region": "us-east-1",
                                 "endpoint": "http://localhost:2668";
-                            },
-                            "llm_provider": "openai"
-                        }
-                    },
-                    "upstream": {
-                        "type": "roundrobin",
-                        "nodes": {
-                            "127.0.0.1:1980": 1
+                            }
                         }
                     }
                 }]]
@@ -81,12 +79,12 @@ passed
 
 
 
-=== TEST 2: request should fail
+=== TEST 2: request fails when Comprehend is unreachable
 --- request
-POST /echo
-toxic
+POST /chat
+{ "messages": [ { "role": "user", "content": "toxic" } ] }
 --- error_code: 500
 --- response_body chomp
-Comprehend:detectToxicContent() failed to connect to 'http://localhost:2668': 
connection refused
+failed to send request to http://localhost: Comprehend:detectToxicContent() 
failed to connect to 'http://localhost:2668': connection refused
 --- error_log
 failed to send request to http://localhost: Comprehend:detectToxicContent() 
failed to connect to 'http://localhost:2668': connection refused


Reply via email to