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
|
return &ms
|
||||||
}
|
}
|
||||||
|
|
||||||
// providerAdapter 描述某个 provider 在 challenge 检测中需要的 4 件事:
|
// providerAdapter 描述某个 provider 在 challenge 检测中需要的几件事:
|
||||||
// - 拼出请求路径(含 model 占位)
|
// - 拼出请求路径(含 model 占位)
|
||||||
// - 序列化请求体
|
// - 序列化请求体
|
||||||
// - 构造鉴权头
|
// - 构造鉴权头
|
||||||
// - 从响应 JSON 中按 path 提取文本(gjson path)
|
// - 从响应 JSON 中提取文本(默认按 gjson path;需要时可自定义)
|
||||||
//
|
//
|
||||||
// 加新 provider 只需要在 providerAdapters 里增加一个条目,无需触碰 callProvider / validateProvider。
|
// 加新 provider 只需要在 providerAdapters 里增加一个条目,无需触碰 callProvider / validateProvider。
|
||||||
type providerAdapter struct {
|
type providerAdapter struct {
|
||||||
@@ -161,6 +161,7 @@ type providerAdapter struct {
|
|||||||
buildBody func(model, prompt string) ([]byte, error)
|
buildBody func(model, prompt string) ([]byte, error)
|
||||||
buildHeaders func(apiKey string) map[string]string
|
buildHeaders func(apiKey string) map[string]string
|
||||||
textPath string // gjson 提取响应文本的 path
|
textPath string // gjson 提取响应文本的 path
|
||||||
|
extractText func([]byte) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// providerAdapters 全部已支持的 provider。键值即 MonitorProvider* 字符串。
|
// providerAdapters 全部已支持的 provider。键值即 MonitorProvider* 字符串。
|
||||||
@@ -184,7 +185,7 @@ var providerAdapters = map[string]providerAdapter{
|
|||||||
"anthropic-version": monitorAnthropicAPIVersion,
|
"anthropic-version": monitorAnthropicAPIVersion,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
textPath: "content.0.text",
|
extractText: extractAnthropicMonitorText,
|
||||||
},
|
},
|
||||||
MonitorProviderGemini: {
|
MonitorProviderGemini: {
|
||||||
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
|
// 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 {
|
if provider == MonitorProviderOpenAI && apiMode == MonitorAPIModeResponses {
|
||||||
return extractOpenAIResponsesText(respBytes), string(respBytes), status, nil
|
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 文本。
|
// 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)
|
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