Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions plugins/wasm-go/extensions/ai-security-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ description: 阿里云内容安全检测
| `requestContentJsonPath` | string | optional | `messages.@reverse.0.content` | 指定要检测内容在请求body中的jsonpath |
| `responseContentJsonPath` | string | optional | `choices.0.message.content` | 指定要检测内容在响应body中的jsonpath |
| `responseStreamContentJsonPath` | string | optional | `choices.0.delta.content` | 指定要检测内容在流式响应body中的jsonpath |
| `responseContentFallbackJsonPaths` | array | optional | [`choices.0.message.content`, `content.#(type=="text")#.text`] | 当 `responseContentJsonPath` 提取为空时,按顺序尝试这些兜底路径;与主路径相同的项会自动跳过;显式配置为空数组 `[]` 可禁用兜底 |
| `responseStreamContentFallbackJsonPaths` | array | optional | [`choices.0.delta.content`, `delta.text`] | 当 `responseStreamContentJsonPath` 提取为空时,按顺序尝试这些流式兜底路径;与主路径相同的项会自动跳过;显式配置为空数组 `[]` 可禁用兜底 |
| `denyCode` | int | optional | 200 | 指定内容非法时的响应状态码 |
| `denyMessage` | string | optional | openai格式的流式/非流式响应 | 指定内容非法时的响应内容 |
| `protocol` | string | optional | openai | 协议格式,非openai协议填`original` |
Expand Down Expand Up @@ -211,6 +213,34 @@ denyMessage: "很抱歉,我无法回答您的问题"
protocol: original
```

### 配置响应内容兜底提取路径

当主路径提取不到内容时,可按优先级顺序配置兜底路径,兼容多种返回协议:

```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: "green-cip.cn-shanghai.aliyuncs.com"
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkResponse: true
responseContentJsonPath: "choices.0.message.content"
responseStreamContentJsonPath: "choices.0.delta.content"
responseContentFallbackJsonPaths:
- "output.text"
- 'content.#(type=="text")#.text'
responseStreamContentFallbackJsonPaths:
- "payload.delta"
- "delta.text"
```

如需严格模式(主路径未命中即跳过,不走兜底),可显式关闭兜底:

```yaml
responseContentFallbackJsonPaths: []
responseStreamContentFallbackJsonPaths: []
```

## 可观测
### Metric
ai-security-guard 插件提供了以下监控指标:
Expand Down
30 changes: 30 additions & 0 deletions plugins/wasm-go/extensions/ai-security-guard/README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Plugin Priority: `300`
| `requestContentJsonPath` | string | optional | `messages.@reverse.0.content` | Specify the jsonpath of the content to be detected in the request body |
| `responseContentJsonPath` | string | optional | `choices.0.message.content` | Specify the jsonpath of the content to be detected in the response body |
| `responseStreamContentJsonPath` | string | optional | `choices.0.delta.content` | Specify the jsonpath of the content to be detected in the streaming response body |
| `responseContentFallbackJsonPaths` | array | optional | [`choices.0.message.content`, `content.#(type=="text")#.text`] | Fallback paths tried in order when `responseContentJsonPath` extracts empty content; entries equal to the primary path are skipped automatically; set to `[]` to disable fallback explicitly |
| `responseStreamContentFallbackJsonPaths` | array | optional | [`choices.0.delta.content`, `delta.text`] | Streaming fallback paths tried in order when `responseStreamContentJsonPath` extracts empty content; entries equal to the primary path are skipped automatically; set to `[]` to disable fallback explicitly |
| `denyCode` | int | optional | 200 | Response status code when the specified content is illegal |
| `denyMessage` | string | optional | Drainage/non-streaming response in openai format, the answer content is the suggested answer from Alibaba Cloud content security | Response content when the specified content is illegal |
| `protocol` | string | optional | openai | protocol format, `openai` or `original` |
Expand Down Expand Up @@ -129,6 +131,34 @@ checkRequest: true
checkResponse: true
```

### Configure response fallback extraction paths

When primary extraction paths are empty, you can configure ordered fallback paths to support multiple response formats:

```yaml
serviceName: safecheck.dns
servicePort: 443
serviceHost: green-cip.cn-shanghai.aliyuncs.com
accessKey: "XXXXXXXXX"
secretKey: "XXXXXXXXXXXXXXX"
checkResponse: true
responseContentJsonPath: "choices.0.message.content"
responseStreamContentJsonPath: "choices.0.delta.content"
responseContentFallbackJsonPaths:
- "output.text"
- 'content.#(type=="text")#.text'
responseStreamContentFallbackJsonPaths:
- "payload.delta"
- "delta.text"
```

To enforce strict mode (no fallback), configure both fields as empty arrays:

```yaml
responseContentFallbackJsonPaths: []
responseStreamContentFallbackJsonPaths: []
```

## Observability
### Metric
ai-security-guard plugin provides following metrics:
Expand Down
121 changes: 89 additions & 32 deletions plugins/wasm-go/extensions/ai-security-guard/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ const (
DefaultTextModerationPlusTextOutputCheckService = "llm_response_moderation"
)

var (
// Keep these defaults aligned with previous hardcoded fallback extraction behavior.
defaultResponseFallbackJsonPaths = []string{
"choices.0.message.content",
`content.#(type=="text")#.text`,
}
defaultStreamingResponseFallbackJsonPaths = []string{
"choices.0.delta.content",
"delta.text",
}
)

func DefaultResponseFallbackJsonPaths() []string {
return append([]string(nil), defaultResponseFallbackJsonPaths...)
}

func DefaultStreamingResponseFallbackJsonPaths() []string {
return append([]string(nil), defaultStreamingResponseFallbackJsonPaths...)
}

// api types

const (
Expand Down Expand Up @@ -143,38 +163,40 @@ func (m *Matcher) match(consumer string) bool {
}

type AISecurityConfig struct {
Client wrapper.HttpClient
Host string
AK string
SK string
Token string
Action string
CheckRequest bool
CheckRequestImage bool
RequestCheckService string
RequestImageCheckService string
RequestContentJsonPath string
CheckResponse bool
ResponseCheckService string
ResponseImageCheckService string
ResponseContentJsonPath string
ResponseStreamContentJsonPath string
DenyCode int64
DenyMessage string
ProtocolOriginal bool
RiskLevelBar string
ContentModerationLevelBar string
PromptAttackLevelBar string
SensitiveDataLevelBar string
MaliciousUrlLevelBar string
ModelHallucinationLevelBar string
CustomLabelLevelBar string
Timeout uint32
BufferLimit int
Metrics map[string]proxywasm.MetricCounter
ConsumerRequestCheckService []map[string]interface{}
ConsumerResponseCheckService []map[string]interface{}
ConsumerRiskLevel []map[string]interface{}
Client wrapper.HttpClient
Host string
AK string
SK string
Token string
Action string
CheckRequest bool
CheckRequestImage bool
RequestCheckService string
RequestImageCheckService string
RequestContentJsonPath string
CheckResponse bool
ResponseCheckService string
ResponseImageCheckService string
ResponseContentJsonPath string
ResponseStreamContentJsonPath string
ResponseContentFallbackJsonPaths []string
ResponseStreamContentFallbackJsonPaths []string
DenyCode int64
DenyMessage string
ProtocolOriginal bool
RiskLevelBar string
ContentModerationLevelBar string
PromptAttackLevelBar string
SensitiveDataLevelBar string
MaliciousUrlLevelBar string
ModelHallucinationLevelBar string
CustomLabelLevelBar string
Timeout uint32
BufferLimit int
Metrics map[string]proxywasm.MetricCounter
ConsumerRequestCheckService []map[string]interface{}
ConsumerResponseCheckService []map[string]interface{}
ConsumerRiskLevel []map[string]interface{}
// text_generation, image_generation, etc.
ApiType string
// openai, qwen, comfyui, etc.
Expand Down Expand Up @@ -287,6 +309,16 @@ func (config *AISecurityConfig) Parse(json gjson.Result) error {
if obj := json.Get("responseStreamContentJsonPath"); obj.Exists() {
config.ResponseStreamContentJsonPath = obj.String()
}
if paths, exists, err := parseOptionalStringArrayConfig(json, "responseContentFallbackJsonPaths"); err != nil {
return err
} else if exists {
config.ResponseContentFallbackJsonPaths = paths
}
if paths, exists, err := parseOptionalStringArrayConfig(json, "responseStreamContentFallbackJsonPaths"); err != nil {
return err
} else if exists {
config.ResponseStreamContentFallbackJsonPaths = paths
}
if obj := json.Get("contentModerationLevelBar"); obj.Exists() {
config.ContentModerationLevelBar = obj.String()
if LevelToInt(config.ContentModerationLevelBar) <= 0 {
Expand Down Expand Up @@ -448,6 +480,29 @@ func parseDimensionAction(json gjson.Result, fieldName string) (string, error) {
return "", nil
}

func parseOptionalStringArrayConfig(json gjson.Result, fieldName string) ([]string, bool, error) {
obj := json.Get(fieldName)
if !obj.Exists() {
return nil, false, nil
}
if !obj.IsArray() {
return nil, true, fmt.Errorf("invalid %s, value must be an array of non-empty strings", fieldName)
}
items := obj.Array()
paths := make([]string, 0, len(items))
for _, item := range items {
if item.Type != gjson.String {
return nil, true, fmt.Errorf("invalid %s, value must be an array of non-empty strings", fieldName)
}
path := strings.TrimSpace(item.String())
if path == "" {
return nil, true, fmt.Errorf("invalid %s, value must be an array of non-empty strings", fieldName)
}
paths = append(paths, path)
}
return paths, true, nil
}

func (config *AISecurityConfig) SetDefaultValues() {
switch config.Action {
case TextModerationPlus:
Expand All @@ -463,6 +518,8 @@ func (config *AISecurityConfig) SetDefaultValues() {
config.RequestContentJsonPath = DefaultRequestJsonPath
config.ResponseContentJsonPath = DefaultResponseJsonPath
config.ResponseStreamContentJsonPath = DefaultStreamingResponseJsonPath
config.ResponseContentFallbackJsonPaths = DefaultResponseFallbackJsonPaths()
config.ResponseStreamContentFallbackJsonPaths = DefaultStreamingResponseFallbackJsonPaths()
config.ContentModerationLevelBar = MaxRisk
config.PromptAttackLevelBar = MaxRisk
config.SensitiveDataLevelBar = S4Sensitive
Expand Down
Loading
Loading