Skip to content

Commit 11c2d58

Browse files
feat(ai-aws-content-moderation): add request_check_roles and request_check_mode (#13773)
1 parent 88f37dc commit 11c2d58

4 files changed

Lines changed: 438 additions & 3 deletions

File tree

apisix/plugins/ai-aws-content-moderation.lua

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ local pairs = pairs
3131
local unpack = unpack
3232
local type = type
3333
local ipairs = ipairs
34+
local next = next
3435
local table = table
3536
local str_byte = string.byte
3637
local str_sub = string.sub
@@ -91,6 +92,31 @@ local schema = {
9192
},
9293
check_request = { type = "boolean", default = true },
9394
check_response = { type = "boolean", default = false },
95+
request_check_roles = {
96+
type = "array",
97+
items = { type = "string", enum = { "user", "tool", "system", "assistant" } },
98+
minItems = 1,
99+
uniqueItems = true,
100+
default = { "user", "tool", "system", "assistant" },
101+
description = "which message roles to moderate on the request side. user, tool " ..
102+
"and assistant follow request_check_mode; system is checked on " ..
103+
"every request because it can be poisoned by malicious ToolCall " ..
104+
"arguments. assistant messages are client-supplied history, so " ..
105+
"they are moderated by default too. Note: tool-result moderation " ..
106+
"applies to OpenAI-compatible formats where the tool output is a " ..
107+
"distinct tool role/item; for Anthropic/Bedrock (tool results are " ..
108+
"nested blocks inside user messages) tool content is not extracted.",
109+
},
110+
request_check_mode = {
111+
type = "string",
112+
enum = { "last", "all" },
113+
default = "all",
114+
description = "which user/tool/assistant messages to moderate: last (only the " ..
115+
"latest consecutive block of selected-role messages) | all (every " ..
116+
"selected-role message). Does not apply to the system role, which " ..
117+
"is always checked. Note that selecting assistant together with " ..
118+
"last widens the block, since assistant turns no longer end it.",
119+
},
94120
request_check_length_limit = {
95121
type = "integer",
96122
minimum = MIN_SEGMENT_BYTES,
@@ -479,7 +505,54 @@ function _M.access(conf, ctx)
479505
return
480506
end
481507

482-
local contents = proto.extract_request_content(request_tab)
508+
local roles = {}
509+
for _, role in ipairs(conf.request_check_roles) do
510+
roles[role] = true
511+
end
512+
-- every role but system is a turn role, extracted through extract_turn_content
513+
local turn_roles = {}
514+
for role in pairs(roles) do
515+
if role ~= "system" then
516+
turn_roles[role] = true
517+
end
518+
end
519+
520+
-- A configured role whose extractor this protocol doesn't implement would
521+
-- otherwise pass unmoderated. Route that through fail_mode instead of
522+
-- silently skipping the configured moderation.
523+
if (roles.system and not proto.extract_system_content)
524+
or (next(turn_roles) and not proto.extract_turn_content) then
525+
local handled, code, body = binding.on_unsupported(
526+
conf.fail_mode, _M.name, ctx,
527+
"protocol cannot extract configured request_check_roles",
528+
HTTP_INTERNAL_SERVER_ERROR,
529+
"protocol " .. (ctx.ai_client_protocol or "unknown")
530+
.. " cannot moderate the configured request_check_roles")
531+
if handled then
532+
return code, body
533+
end
534+
return
535+
end
536+
537+
-- Collect the text of every configured role and score it in one pass:
538+
-- Comprehend takes a flat list of text segments with no role field, so
539+
-- per-role calls would only cost extra requests. system is always included
540+
-- (not subject to request_check_mode, it can be poisoned by malicious
541+
-- ToolCall arguments); the turn roles follow request_check_mode.
542+
local contents = {}
543+
if roles.system then
544+
local system_texts = proto.extract_system_content(request_tab)
545+
for i = 1, #system_texts do
546+
contents[#contents + 1] = system_texts[i]
547+
end
548+
end
549+
if next(turn_roles) then
550+
local turn_texts = proto.extract_turn_content(request_tab,
551+
conf.request_check_mode, turn_roles)
552+
for i = 1, #turn_texts do
553+
contents[#contents + 1] = turn_texts[i]
554+
end
555+
end
483556
local content = table.concat(contents, " ")
484557
if content == "" then
485558
return

docs/en/latest/plugins/ai-aws-content-moderation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import TabItem from '@theme/TabItem';
3838

3939
The `ai-aws-content-moderation` Plugin integrates with [AWS Comprehend](https://aws.amazon.com/comprehend/) to check 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.
4040

41-
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.
41+
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. `request_check_roles` selects which message roles are moderated, and `request_check_mode` can narrow `user`/`tool` moderation to the newest turn so conversation history is not re-scored on every request.
4242

4343
Both directions can be moderated. Set `check_response` to moderate the LLM response as well. For streaming responses, `stream_check_mode` selects between `realtime`, which moderates batches as they arrive and replaces the remainder of the stream once a batch is flagged, and `final_packet`, which moderates the assembled response and annotates the last chunk with `risk_level`. The verdict is also exposed on the request context as `$llm_content_risk_level` (`high` or `none`) for logging.
4444

@@ -62,6 +62,8 @@ The `ai-aws-content-moderation` Plugin should be used with either [`ai-proxy`](.
6262
| `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` exceeding the `moderation_threshold`, the request will be rejected. |
6363
| `check_request` | boolean | False | `true` | | If `true`, moderate the request content. |
6464
| `check_response` | boolean | False | `false` | | If `true`, moderate the LLM response content. |
65+
| `request_check_roles` | array[string] | False | `["user","tool","system","assistant"]` | items are `user`, `tool`, `system`, `assistant` | Which message roles to moderate on the request side. `user`, `tool` and `assistant` follow `request_check_mode`; `system` is checked on every request (it can be poisoned by malicious ToolCall arguments overwriting the system prompt). `assistant` messages in a request are client-supplied history rather than the model's own output, so they are moderated by default as well. Note: tool-result moderation applies to OpenAI-compatible formats where the tool output is a distinct `tool` role/item; for Anthropic and Bedrock (tool results are nested blocks inside user messages) tool content is not extracted. |
66+
| `request_check_mode` | string | False | `all` | `last`, `all` | Which user/tool/assistant messages to moderate. `last`: only the latest consecutive block of selected-role messages (the newest turn). `all`: every selected-role message. Does not apply to `system`, which is always moderated when enabled via `request_check_roles`. Note that `last` combined with `assistant` widens the block rather than narrowing it, because assistant turns no longer end it — drop `assistant` from `request_check_roles` to moderate only the newest turn. |
6567
| `request_check_length_limit` | integer | False | `1000` | [4, 1024] | Maximum bytes of request content per Comprehend text segment. Longer content is split on character boundaries into several segments, which are then batched into as few Comprehend calls as possible. The upper bound is AWS Comprehend's 1 KB per-segment limit. |
6668
| `response_check_length_limit` | integer | False | `1000` | [4, 1024] | Maximum bytes of response content per Comprehend text segment. Longer content is split on character boundaries into several segments, which are then batched into as few Comprehend calls as possible. The upper bound is AWS Comprehend's 1 KB per-segment limit. |
6769
| `stream_check_mode` | string | False | `final_packet` | `realtime`, `final_packet` | Streaming moderation mode, used when `check_response` is `true`. `realtime`: moderate batches while the response streams, replacing the rest of the stream once a batch is flagged. `final_packet`: moderate the assembled response and annotate the last chunk with `risk_level`. |

docs/zh/latest/plugins/ai-aws-content-moderation.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import TabItem from '@theme/TabItem';
4040

4141
`ai-aws-content-moderation` 插件集成了 [AWS Comprehend](https://aws.amazon.com/comprehend/),用于在代理请求到 LLM 时检查请求内容中的有害内容,例如亵渎、仇恨言论、侮辱、骚扰、暴力等,如果评估结果超过配置的阈值则拒绝请求。
4242

43-
该插件是协议感知的:它会从 LLM 请求中提取提示内容(例如 `messages[].content`),仅审核解码后的文本,而不是原始请求体。
43+
该插件是协议感知的:它会从 LLM 请求中提取提示内容(例如 `messages[].content`),仅审核解码后的文本,而不是原始请求体。`request_check_roles` 用于选择审核哪些消息角色,`request_check_mode` 可将 `user`/`tool` 的审核范围收窄到最新一轮,避免每次请求都重复审核历史对话。
4444

4545
`ai-aws-content-moderation` 插件应与 [`ai-proxy`](./ai-proxy.md)[`ai-proxy-multi`](./ai-proxy-multi.md) 插件一起使用,以代理 LLM 请求。
4646

@@ -57,6 +57,8 @@ import TabItem from '@theme/TabItem';
5757
| `moderation_categories` | object || | | 审核类别及其对应阈值的键值对。在每个键值对中,键应为 `PROFANITY``HATE_SPEECH``INSULT``HARASSMENT_OR_ABUSE``SEXUAL``VIOLENCE_OR_THREAT` 之一;阈值应在 0 到 1 之间(包含)。 |
5858
| `moderation_threshold` | number || 0.5 | 0 - 1 | 整体毒性阈值。值越高,允许的有害内容越多。此选项与 `moderation_categories` 中的单独类别阈值不同。例如,如果 `moderation_categories` 中设置了 `PROFANITY` 阈值为 `0.5`,而请求的 `PROFANITY` 分数为 `0.1`,则请求不会超过类别阈值。但如果请求的其他类别(如 `SEXUAL``VIOLENCE_OR_THREAT`)超过了 `moderation_threshold`,则请求将被拒绝。 |
5959
| `check_request` | boolean || `true` | | 如果为 `true`,则审核请求内容。 |
60+
| `request_check_roles` | array[string] || `["user","tool","system","assistant"]` | 取值为 `user``tool``system``assistant` | 请求侧审核哪些消息角色。`user``tool``assistant` 遵循 `request_check_mode``system` 每次请求都审核(其可能被恶意 ToolCall 参数覆盖篡改)。请求中的 `assistant` 消息由客户端提供,而非模型自身的输出,因此默认也会被审核。注意:tool 结果审核适用于 OpenAI 兼容格式(tool 输出为独立的 `tool` 角色/项);Anthropic、Bedrock 的 tool 结果以嵌套 block 形式存在于 user 消息中,其内容不会被抽取。 |
61+
| `request_check_mode` | string || `all` | `last``all` | 审核哪些 user/tool/assistant 消息。`last`:仅审核最后一段连续的所选角色消息(最新一轮);`all`:审核所有所选角色消息。不作用于 `system`——只要通过 `request_check_roles` 启用,`system` 每次都审核。注意:`last``assistant` 同时使用会扩大而非缩小审核范围,因为 assistant 消息不再中断该连续块;若只想审核最新一轮,请从 `request_check_roles` 中移除 `assistant`|
6062
| `deny_code` | integer || `200` | [200, 599] | 请求被拒绝时返回的 HTTP 状态码。默认为 `200`,使兼容 provider 的拒绝响应在客户端 SDK 中被解析为正常补全;设置为 4xx 可将拒绝暴露为 HTTP 错误。 |
6163
| `deny_message` | string || | | 请求被拒绝时返回的消息。未设置时,返回审核原因(例如 `request body exceeds toxicity threshold`)。 |
6264
| `fail_mode` | string || `skip` | `skip``warn``error` | 当请求未经过 `ai-proxy`/`ai-proxy-multi`,因而无法作为 AI 请求进行审核时的处理行为。`skip`:放行请求且不做检查;`warn`:放行并记录 warning 日志;`error`:拒绝请求。 |

0 commit comments

Comments
 (0)