mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 18:06:50 +08:00
feat(executor): sanitize web search tool domains to meet Anthropic requirements
- Added `sanitizeClaudeWebSearchDomains` to remove empty `allowed_domains` and `blocked_domains` fields for built-in web_search tools, addressing ambiguity errors from Anthropic. - Integrated domain sanitization into the Claude message preparation pipeline. - Added test cases to validate correct handling of empty and non-empty domain fields across various tool types. Closes: #2681
This commit is contained in:
@@ -48,9 +48,38 @@ const claudeToolPrefix = ""
|
||||
func sanitizeClaudeMessagesForClaudeUpstreamWithDebug(ctx context.Context, body []byte, baseModel string) []byte {
|
||||
sanitized, report := sigcompat.SanitizeClaudeMessagesForClaudeUpstream(body, baseModel)
|
||||
logClaudeSignatureSanitizeReport(ctx, baseModel, report)
|
||||
sanitized = sanitizeClaudeWebSearchDomains(sanitized)
|
||||
return sanitized
|
||||
}
|
||||
|
||||
// sanitizeClaudeWebSearchDomains removes empty allowed_domains/blocked_domains
|
||||
// arrays from built-in web_search tools. Some clients (e.g. litellm) emit an
|
||||
// empty array instead of omitting the field, and Anthropic rejects it with
|
||||
// "Empty list of domains is ambiguous. Provide at least one domain or null.".
|
||||
// Deleting the key is equivalent to leaving it unset.
|
||||
func sanitizeClaudeWebSearchDomains(body []byte) []byte {
|
||||
tools := gjson.GetBytes(body, "tools")
|
||||
if !tools.Exists() || !tools.IsArray() {
|
||||
return body
|
||||
}
|
||||
tools.ForEach(func(index, tool gjson.Result) bool {
|
||||
if !strings.HasPrefix(tool.Get("type").String(), "web_search_") {
|
||||
return true
|
||||
}
|
||||
for _, field := range []string{"allowed_domains", "blocked_domains"} {
|
||||
value := tool.Get(field)
|
||||
if value.Exists() && value.IsArray() && len(value.Array()) == 0 {
|
||||
path := fmt.Sprintf("tools.%d.%s", index.Int(), field)
|
||||
if updated, errDelete := sjson.DeleteBytes(body, path); errDelete == nil {
|
||||
body = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
func logClaudeSignatureSanitizeReport(ctx context.Context, baseModel string, report sigcompat.SignatureSanitizeReport) {
|
||||
if report.DroppedBlocks == 0 && report.DroppedSignatures == 0 && report.ReplacedSignatures == 0 {
|
||||
return
|
||||
|
||||
@@ -636,6 +636,36 @@ func TestApplyClaudeToolPrefix_WithToolReference(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeClaudeWebSearchDomains(t *testing.T) {
|
||||
// Mirrors the litellm payload from issue #2681: a non-empty allowed_domains
|
||||
// alongside an empty blocked_domains, which Anthropic rejects as ambiguous.
|
||||
input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search","allowed_domains":["anthropic.com"],"blocked_domains":[],"max_uses":8}]}`)
|
||||
out := sanitizeClaudeWebSearchDomains(input)
|
||||
|
||||
if gjson.GetBytes(out, "tools.0.blocked_domains").Exists() {
|
||||
t.Fatalf("empty blocked_domains should be removed: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.allowed_domains").Array(); len(got) != 1 || got[0].String() != "anthropic.com" {
|
||||
t.Fatalf("non-empty allowed_domains should be preserved: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.0.max_uses").Int(); got != 8 {
|
||||
t.Fatalf("max_uses should be preserved: got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeClaudeWebSearchDomains_LeavesNonBuiltinAndNonEmpty(t *testing.T) {
|
||||
// Empty arrays on non-web_search tools must be left untouched.
|
||||
input := []byte(`{"tools":[{"type":"custom","name":"x","blocked_domains":[]},{"type":"web_search_20250305","name":"web_search","blocked_domains":["evil.com"]}]}`)
|
||||
out := sanitizeClaudeWebSearchDomains(input)
|
||||
|
||||
if !gjson.GetBytes(out, "tools.0.blocked_domains").Exists() {
|
||||
t.Fatalf("non-web_search tool fields should be untouched: %s", string(out))
|
||||
}
|
||||
if got := gjson.GetBytes(out, "tools.1.blocked_domains").Array(); len(got) != 1 || got[0].String() != "evil.com" {
|
||||
t.Fatalf("non-empty blocked_domains should be preserved: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaudeToolPrefix_SkipsBuiltinTools(t *testing.T) {
|
||||
input := []byte(`{"tools":[{"type":"web_search_20250305","name":"web_search"},{"name":"my_custom_tool","input_schema":{"type":"object"}}]}`)
|
||||
out := applyClaudeToolPrefix(input, "proxy_")
|
||||
|
||||
Reference in New Issue
Block a user