package openai import ( "ds2api/internal/toolcall" "regexp" "strings" ) // --- XML tool call support for the streaming sieve --- //nolint:unused // kept as explicit tag inventory for future XML sieve refinements. var xmlToolCallClosingTags = []string{"", "", "", "", "", "", // Agent-style XML tags (Roo Code, Cline, etc.) "", "", "", ""} var xmlToolCallOpeningTags = []string{""}, {""}, {""}, {""}, {""}, {""}, // Agent-style: these are XML "tool call" patterns from coding agents. // They get captured → parsed. If parsing fails, the block is consumed // (swallowed) to prevent raw XML from leaking to the client. {""}, {""}, {""}, } // xmlToolCallBlockPattern matches a complete XML tool call block (wrapper or standalone). // //nolint:unused // reserved for future fast-path XML block detection. var xmlToolCallBlockPattern = regexp.MustCompile(`(?is)(\s*(?:.*?)\s*|\s*(?:.*?)\s*|]*>(?:.*?)|]*>(?:.*?)|(?:.*?)|(?:.*?)|(?:.*?)|(?:.*?))`) // xmlToolTagsToDetect is the set of XML tag prefixes used by findToolSegmentStart. var xmlToolTagsToDetect = []string{"", "", "", "", // Agent-style tags "", "", ""} // consumeXMLToolCapture tries to extract complete XML tool call blocks from captured text. func consumeXMLToolCapture(captured string, toolNames []string) (prefix string, calls []toolcall.ParsedToolCall, suffix string, ready bool) { lower := strings.ToLower(captured) // Find the FIRST matching open/close pair, preferring wrapper tags. // Tag pairs are ordered longest-first (e.g. 0 { prefixPart, suffixPart = trimWrappingJSONFence(prefixPart, suffixPart) return prefixPart, parsed, suffixPart, true } // If this block does not look like an executable tool-call payload, // pass it through as normal content (e.g. user-requested XML snippets). if !looksLikeExecutableXMLToolCallBlock(xmlBlock, pair.open) { return prefixPart + xmlBlock, nil, suffixPart, true } // Looks like XML tool syntax but failed to parse — consume it to avoid leak. return prefixPart, nil, suffixPart, true } return "", nil, "", false } func looksLikeExecutableXMLToolCallBlock(xmlBlock, openTag string) bool { lower := strings.ToLower(xmlBlock) // Agent wrapper tags are always treated as internal tool-call wrappers. switch openTag { case "' in the tail, the tag is closed — not partial. if strings.Contains(tail, ">") { return -1 } lowerTail := strings.ToLower(tail) // Check if the tail is a prefix of any known XML tool tag. for _, tag := range xmlToolCallOpeningTags { tagWithLT := tag if !strings.HasPrefix(tagWithLT, "<") { tagWithLT = "<" + tagWithLT } if strings.HasPrefix(tagWithLT, lowerTail) { return lastLT } } return -1 } // looksLikeXMLToolTagFragment returns true if s looks like a fragment from a // split XML tool call tag — for example "tool_calls>" or "/tool_call>\n". // These fragments arise when '<' was consumed separately and the tail remains. func looksLikeXMLToolTagFragment(s string) bool { trimmed := strings.TrimSpace(s) if trimmed == "" { return false } lower := strings.ToLower(trimmed) // Check for closing tag tails like "tool_calls>" or "/tool_calls>" fragments := []string{ "tool_calls>", "tool_call>", "/tool_calls>", "/tool_call>", "function_calls>", "function_call>", "/function_calls>", "/function_call>", "invoke>", "/invoke>", "tool_use>", "/tool_use>", "tool_name>", "/tool_name>", "parameters>", "/parameters>", // Agent-style tag fragments "attempt_completion>", "/attempt_completion>", "ask_followup_question>", "/ask_followup_question>", "new_task>", "/new_task>", "result>", "/result>", } for _, f := range fragments { if strings.Contains(lower, f) { return true } } return false }