mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
fix: keep execute_command args from xml parameters blocks
This commit is contained in:
@@ -9,7 +9,7 @@ func filterLeakedContentFilterParts(parts []ContentPart) []ContentPart {
|
||||
out := make([]ContentPart, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
cleaned := stripLeakedContentFilterSuffix(p.Text)
|
||||
if strings.TrimSpace(cleaned) == "" {
|
||||
if shouldDropCleanedLeakedChunk(cleaned) {
|
||||
continue
|
||||
}
|
||||
p.Text = cleaned
|
||||
@@ -27,5 +27,19 @@ func stripLeakedContentFilterSuffix(text string) string {
|
||||
if idx < 0 {
|
||||
return text
|
||||
}
|
||||
return strings.TrimRight(text[:idx], " \t\r\n")
|
||||
// Keep "\n" so we don't collapse line structure when the upstream model
|
||||
// appends leaked CONTENT_FILTER markers after a line break.
|
||||
return strings.TrimRight(text[:idx], " \t\r")
|
||||
}
|
||||
|
||||
func shouldDropCleanedLeakedChunk(cleaned string) bool {
|
||||
if cleaned == "" {
|
||||
return true
|
||||
}
|
||||
// Preserve newline-only chunks to avoid dropping legitimate line breaks
|
||||
// before a leaked CONTENT_FILTER suffix.
|
||||
if strings.Contains(cleaned, "\n") {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(cleaned) == ""
|
||||
}
|
||||
|
||||
@@ -102,3 +102,23 @@ func TestParseDeepSeekContentLineContentTextEqualContentFilterDoesNotStop(t *tes
|
||||
t.Fatalf("did not expect content-filter stop for content text: %#v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeepSeekContentLinePreservesTrailingNewlineBeforeLeakedContentFilter(t *testing.T) {
|
||||
res := ParseDeepSeekContentLine([]byte("data: {\"p\":\"response/content\",\"v\":\"line1\\nCONTENT_FILTERblocked\"}"), false, "text")
|
||||
if !res.Parsed || res.Stop {
|
||||
t.Fatalf("expected parsed non-stop result: %#v", res)
|
||||
}
|
||||
if len(res.Parts) != 1 || res.Parts[0].Text != "line1\n" {
|
||||
t.Fatalf("expected trailing newline preserved, got %#v", res.Parts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeepSeekContentLineKeepsNewlineOnlyChunkBeforeLeakedContentFilter(t *testing.T) {
|
||||
res := ParseDeepSeekContentLine([]byte("data: {\"p\":\"response/content\",\"v\":\"\\nCONTENT_FILTERblocked\"}"), false, "text")
|
||||
if !res.Parsed || res.Stop {
|
||||
t.Fatalf("expected parsed non-stop result: %#v", res)
|
||||
}
|
||||
if len(res.Parts) != 1 || res.Parts[0].Text != "\n" {
|
||||
t.Fatalf("expected newline-only chunk preserved, got %#v", res.Parts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ var toolUseFunctionPattern = regexp.MustCompile(`(?is)<tool_use>\s*<function\s+n
|
||||
var toolUseNameParametersPattern = regexp.MustCompile(`(?is)<tool_use>\s*<tool_name>\s*([^<]+?)\s*</tool_name>\s*<parameters>\s*(.*?)\s*</parameters>\s*</tool_use>`)
|
||||
var toolUseFunctionNameParametersPattern = regexp.MustCompile(`(?is)<tool_use>\s*<function_name>\s*([^<]+?)\s*</function_name>\s*<parameters>\s*(.*?)\s*</parameters>\s*</tool_use>`)
|
||||
var toolUseToolNameBodyPattern = regexp.MustCompile(`(?is)<tool_use>\s*<tool_name>\s*([^<]+?)\s*</tool_name>\s*(.*?)\s*</tool_use>`)
|
||||
var xmlToolNamePatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?is)<(?:[a-z0-9_:-]+:)?tool_name\b[^>]*>(.*?)</(?:[a-z0-9_:-]+:)?tool_name>`),
|
||||
regexp.MustCompile(`(?is)<(?:[a-z0-9_:-]+:)?function_name\b[^>]*>(.*?)</(?:[a-z0-9_:-]+:)?function_name>`),
|
||||
regexp.MustCompile(`(?is)<(?:[a-z0-9_:-]+:)?name\b[^>]*>(.*?)</(?:[a-z0-9_:-]+:)?name>`),
|
||||
}
|
||||
|
||||
func parseXMLToolCalls(text string) []ParsedToolCall {
|
||||
matches := xmlToolCallPattern.FindAllString(text, -1)
|
||||
@@ -81,9 +86,9 @@ func parseSingleXMLToolCall(block string) (ParsedToolCall, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(extractXMLToolNameByRegex(inner))
|
||||
params := extractXMLToolParamsByRegex(inner)
|
||||
dec := xml.NewDecoder(strings.NewReader(block))
|
||||
name := ""
|
||||
params := map[string]any{}
|
||||
inParams := false
|
||||
inTool := false
|
||||
for {
|
||||
@@ -170,6 +175,29 @@ func parseSingleXMLToolCall(block string) (ParsedToolCall, bool) {
|
||||
return ParsedToolCall{Name: strings.TrimSpace(name), Input: params}, true
|
||||
}
|
||||
|
||||
func extractXMLToolNameByRegex(inner string) string {
|
||||
for _, pattern := range xmlToolNamePatterns {
|
||||
if m := pattern.FindStringSubmatch(inner); len(m) >= 2 {
|
||||
if v := strings.TrimSpace(stripTagText(m[1])); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractXMLToolParamsByRegex(inner string) map[string]any {
|
||||
raw := findMarkupTagValue(inner, toolCallMarkupArgsTagNames, toolCallMarkupArgsPatternByTag)
|
||||
if raw == "" {
|
||||
return map[string]any{}
|
||||
}
|
||||
parsed := parseMarkupInput(raw)
|
||||
if parsed == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseFunctionCallTagStyle(text string) (ParsedToolCall, bool) {
|
||||
m := functionCallPattern.FindStringSubmatch(text)
|
||||
if len(m) < 2 {
|
||||
|
||||
@@ -176,6 +176,21 @@ func TestParseToolCallsSupportsCanonicalXMLParametersJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsSupportsXMLParametersJSONWithAmpersandCommand(t *testing.T) {
|
||||
text := `<tool_calls><tool_call><tool_name>execute_command</tool_name><parameters>{"command":"sshpass -p 'xxx' ssh -o StrictHostKeyChecking=no -p 1111 root@111.111.111.111 'cd /root && git clone https://github.com/ericc-ch/copilot-api.git'","cwd":null,"timeout":null}</parameters></tool_call></tool_calls>`
|
||||
calls := ParseToolCalls(text, []string{"execute_command"})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %#v", calls)
|
||||
}
|
||||
if calls[0].Name != "execute_command" {
|
||||
t.Fatalf("expected tool name execute_command, got %q", calls[0].Name)
|
||||
}
|
||||
cmd, _ := calls[0].Input["command"].(string)
|
||||
if !strings.Contains(cmd, "&& git clone") {
|
||||
t.Fatalf("expected command to keep && segment, got %#v", calls[0].Input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseToolCallsPrefersJSONPayloadOverIncidentalXMLInString(t *testing.T) {
|
||||
text := `{"tool_calls":[{"name":"search","input":{"q":"latest <tool_call><tool_name>wrong</tool_name><parameters>{\"x\":1}</parameters></tool_call>"}}]}`
|
||||
calls := ParseToolCallsDetailed(text, []string{"search"}).Calls
|
||||
|
||||
Reference in New Issue
Block a user