fix(monitor): extract anthropic text blocks
(cherry picked from commit 76cf1ee834c78e3b15dee6fc23244fbae94a4bd1)
This commit is contained in:
committed by
Heatherm Huang
parent
b1a6b80267
commit
1a855d3e0e
@@ -149,11 +149,11 @@ func pingEndpointOrigin(ctx context.Context, endpoint string) *int {
|
||||
return &ms
|
||||
}
|
||||
|
||||
// providerAdapter 描述某个 provider 在 challenge 检测中需要的 4 件事:
|
||||
// providerAdapter 描述某个 provider 在 challenge 检测中需要的几件事:
|
||||
// - 拼出请求路径(含 model 占位)
|
||||
// - 序列化请求体
|
||||
// - 构造鉴权头
|
||||
// - 从响应 JSON 中按 path 提取文本(gjson path)
|
||||
// - 从响应 JSON 中提取文本(默认按 gjson path;需要时可自定义)
|
||||
//
|
||||
// 加新 provider 只需要在 providerAdapters 里增加一个条目,无需触碰 callProvider / validateProvider。
|
||||
type providerAdapter struct {
|
||||
@@ -161,6 +161,7 @@ type providerAdapter struct {
|
||||
buildBody func(model, prompt string) ([]byte, error)
|
||||
buildHeaders func(apiKey string) map[string]string
|
||||
textPath string // gjson 提取响应文本的 path
|
||||
extractText func([]byte) string
|
||||
}
|
||||
|
||||
// providerAdapters 全部已支持的 provider。键值即 MonitorProvider* 字符串。
|
||||
@@ -184,7 +185,7 @@ var providerAdapters = map[string]providerAdapter{
|
||||
"anthropic-version": monitorAnthropicAPIVersion,
|
||||
}
|
||||
},
|
||||
textPath: "content.0.text",
|
||||
extractText: extractAnthropicMonitorText,
|
||||
},
|
||||
MonitorProviderGemini: {
|
||||
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
|
||||
@@ -293,7 +294,34 @@ func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt
|
||||
if provider == MonitorProviderOpenAI && apiMode == MonitorAPIModeResponses {
|
||||
return extractOpenAIResponsesText(respBytes), string(respBytes), status, nil
|
||||
}
|
||||
return gjson.GetBytes(respBytes, adapter.textPath).String(), string(respBytes), status, nil
|
||||
return extractMonitorResponseText(adapter, respBytes), string(respBytes), status, nil
|
||||
}
|
||||
|
||||
func extractMonitorResponseText(adapter providerAdapter, respBytes []byte) string {
|
||||
if adapter.extractText != nil {
|
||||
return adapter.extractText(respBytes)
|
||||
}
|
||||
return gjson.GetBytes(respBytes, adapter.textPath).String()
|
||||
}
|
||||
|
||||
func extractAnthropicMonitorText(respBytes []byte) string {
|
||||
content := gjson.GetBytes(respBytes, "content")
|
||||
if !content.IsArray() {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := make([]string, 0, 1)
|
||||
content.ForEach(func(_, item gjson.Result) bool {
|
||||
if item.Get("type").String() != "text" {
|
||||
return true
|
||||
}
|
||||
text := strings.TrimSpace(item.Get("text").String())
|
||||
if text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// extractOpenAIResponsesText 聚合 Responses API 的最终 assistant 文本。
|
||||
|
||||
@@ -451,3 +451,50 @@ func TestRunCheckForModel_ReplaceMode_EmptyResponseIsFailed(t *testing.T) {
|
||||
t.Errorf("failure message should hint replace-mode, got %q", res.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractAnthropicMonitorText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "text block after thinking",
|
||||
body: `{"content":[{"type":"thinking","thinking":""},{"type":"text","text":"2"}]}`,
|
||||
want: "2",
|
||||
},
|
||||
{
|
||||
name: "single text block",
|
||||
body: `{"content":[{"type":"text","text":"2"}]}`,
|
||||
want: "2",
|
||||
},
|
||||
{
|
||||
name: "thinking only",
|
||||
body: `{"content":[{"type":"thinking","thinking":""}]}`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "multiple text blocks",
|
||||
body: `{"content":[{"type":"text","text":"answer"},{"type":"tool_use","name":"x"},{"type":"text","text":"2"}]}`,
|
||||
want: "answer\n2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractAnthropicMonitorText([]byte(tt.body))
|
||||
if got != tt.want {
|
||||
t.Fatalf("extractAnthropicMonitorText() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChallenge_AnthropicTextAfterThinking(t *testing.T) {
|
||||
body := []byte(`{"content":[{"type":"thinking","thinking":""},{"type":"text","text":"答案是 2"}]}`)
|
||||
respText := extractAnthropicMonitorText(body)
|
||||
|
||||
if !validateChallenge(respText, "2") {
|
||||
t.Fatalf("validateChallenge(%q, %q) = false, want true", respText, "2")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user