末尾 role 检查方案:当 messages / input / contents 数组末尾一项不是用户消息 (而是 assistant、tool / function_call_output 等)时,直接跳过内容审计, 从而避免 Agent 工具循环中同一用户输入被反复审计、计费、写日志。 Fixes #2678
180 lines
5.4 KiB
Go
180 lines
5.4 KiB
Go
package service
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// 当数组末尾不是用户消息时(典型场景:Agent 工具循环结束于 tool/assistant),
|
||
// 应直接跳过审计——不再回溯查找历史中的某条用户消息。
|
||
|
||
func TestExtractContentModerationInput_AnthropicAgentToolLoopSkipsAudit(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"user","content":"调用一下天气工具"},
|
||
{"role":"assistant","content":[{"type":"tool_use","id":"tool_1","name":"weather","input":{}}]},
|
||
{"role":"user","content":[{"type":"tool_result","tool_use_id":"tool_1","content":"晴 25 度"}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
|
||
|
||
require.Empty(t, input.Text)
|
||
require.Empty(t, input.Images)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_AnthropicFirstTurnExtractsUser(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"user","content":"Q1"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
|
||
|
||
require.Equal(t, "Q1", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_AnthropicMultiTurnExtractsLatestUser(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"user","content":"Q1"},
|
||
{"role":"assistant","content":"A1"},
|
||
{"role":"user","content":"Q2"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
|
||
|
||
require.Equal(t, "Q2", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_AnthropicStreamResendExtractsResend(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"user","content":"原问题"},
|
||
{"role":"assistant","content":"部分回答……"},
|
||
{"role":"user","content":"重发"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
|
||
|
||
require.Equal(t, "重发", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_OpenAIChatAgentToolLoopSkipsAudit(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"system","content":"sys"},
|
||
{"role":"user","content":"列出我的订单"},
|
||
{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"orders","arguments":"{}"}}]},
|
||
{"role":"tool","tool_call_id":"call_1","content":"[]"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIChat, body)
|
||
|
||
require.Empty(t, input.Text)
|
||
require.Empty(t, input.Images)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_OpenAIChatMultiTurnExtractsLatestUser(t *testing.T) {
|
||
body := []byte(`{
|
||
"messages": [
|
||
{"role":"user","content":"Q1"},
|
||
{"role":"assistant","content":"A1"},
|
||
{"role":"user","content":"Q2"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIChat, body)
|
||
|
||
require.Equal(t, "Q2", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_GeminiAgentToolLoopSkipsAudit(t *testing.T) {
|
||
body := []byte(`{
|
||
"contents": [
|
||
{"role":"user","parts":[{"text":"查询天气"}]},
|
||
{"role":"model","parts":[{"functionCall":{"name":"weather","args":{}}}]},
|
||
{"role":"user","parts":[{"functionResponse":{"name":"weather","response":{"temp":25}}}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
|
||
|
||
require.Empty(t, input.Text)
|
||
require.Empty(t, input.Images)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_GeminiFirstTurnExtractsUser(t *testing.T) {
|
||
body := []byte(`{
|
||
"contents": [
|
||
{"role":"user","parts":[{"text":"你好"}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
|
||
|
||
require.Equal(t, "你好", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_GeminiMultiTurnExtractsLatestUser(t *testing.T) {
|
||
body := []byte(`{
|
||
"contents": [
|
||
{"role":"user","parts":[{"text":"Q1"}]},
|
||
{"role":"model","parts":[{"text":"A1"}]},
|
||
{"role":"user","parts":[{"text":"Q2"}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
|
||
|
||
require.Equal(t, "Q2", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_ResponsesAgentToolLoopSkipsAudit(t *testing.T) {
|
||
body := []byte(`{
|
||
"input":[
|
||
{"type":"message","role":"user","content":[{"type":"input_text","text":"运行测试"}]},
|
||
{"type":"function_call","call_id":"call_1","name":"run_tests","arguments":"{}"},
|
||
{"type":"function_call_output","call_id":"call_1","output":"all passed"}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
|
||
|
||
require.Empty(t, input.Text)
|
||
require.Empty(t, input.Images)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_ResponsesLastUserMessageExtracted(t *testing.T) {
|
||
body := []byte(`{
|
||
"input":[
|
||
{"type":"message","role":"user","content":[{"type":"input_text","text":"first"}]},
|
||
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]},
|
||
{"type":"message","role":"user","content":[{"type":"input_text","text":"latest"}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
|
||
|
||
require.Equal(t, "latest", input.Text)
|
||
}
|
||
|
||
func TestExtractContentModerationInput_ResponsesLastIsAssistantSkipped(t *testing.T) {
|
||
body := []byte(`{
|
||
"input":[
|
||
{"type":"message","role":"user","content":[{"type":"input_text","text":"q1"}]},
|
||
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"a1"}]}
|
||
]
|
||
}`)
|
||
|
||
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
|
||
|
||
require.Empty(t, input.Text)
|
||
require.Empty(t, input.Images)
|
||
}
|