package openai import ( "fmt" "strings" ) func scanXMLTextToolCallBlocks(content string) ([]xmlTextToolCallBlock, bool) { if !strings.Contains(strings.ToLower(content), textToolCallOpeningHint) { return nil, false } foundAny := false var blocks []xmlTextToolCallBlock for pos := 0; pos < len(content); { openStart, openEnd, ok := findXMLOpenTag(content, pos, len(content), "tool_call") if !ok { break } foundAny = true closeStart, closeEnd, ok := findXMLCloseTag(content, openEnd, len(content), "tool_call") if !ok { blocks = append(blocks, xmlTextToolCallBlock{ start: openStart, end: len(content), err: fmt.Errorf("malformed tool call: unclosed tool_call block"), }) break } block := parseXMLTextToolCallBlock(content, openStart, openEnd, closeStart, closeEnd) blocks = append(blocks, block) pos = closeEnd } return blocks, foundAny } func parseXMLTextToolCallBlock(content string, blockStart, bodyStart, bodyEnd, blockEnd int) xmlTextToolCallBlock { _, fnOpenEnd, name, ok := findXMLNamedOpenTag(content, bodyStart, bodyEnd, "function") if !ok { return xmlTextToolCallBlock{ start: blockStart, end: blockEnd, err: fmt.Errorf("malformed tool call: missing function block or definition inside tool_call"), } } fnCloseStart, _, ok := findXMLCloseTag(content, fnOpenEnd, bodyEnd, "function") if !ok { return xmlTextToolCallBlock{ start: blockStart, end: blockEnd, err: fmt.Errorf("malformed tool call: missing function block or definition inside tool_call"), } } params := map[string]string{} for pos := fnOpenEnd; pos < fnCloseStart; { paramStart, paramOpenEnd, key, ok := findXMLNamedOpenTag(content, pos, fnCloseStart, "parameter") if !ok { break } paramCloseStart, paramCloseEnd, ok := findXMLCloseTag(content, paramOpenEnd, fnCloseStart, "parameter") if !ok { break } key = strings.TrimSpace(key) if key != "" { params[key] = content[paramOpenEnd:paramCloseStart] } pos = paramCloseEnd if pos <= paramStart { break } } return xmlTextToolCallBlock{ start: blockStart, end: blockEnd, name: strings.TrimSpace(name), params: params, err: nil, } } func findXMLOpenTag(content string, start, limit int, tag string) (int, int, bool) { for pos := start; pos < limit; { idx := strings.IndexByte(content[pos:limit], '<') if idx < 0 { return 0, 0, false } tagStart := pos + idx next := skipASCIISpaces(content, tagStart+1) if next < limit && content[next] == '/' { pos = tagStart + 1 continue } if !matchASCIIFoldAt(content, next, tag) { pos = tagStart + 1 continue } afterTag := next + len(tag) if afterTag < limit && isTextToolIdentifierChar(content[afterTag], false) { pos = tagStart + 1 continue } closeIdx := strings.IndexByte(content[afterTag:limit], '>') if closeIdx < 0 { return 0, 0, false } return tagStart, afterTag + closeIdx + 1, true } return 0, 0, false } func findXMLNamedOpenTag(content string, start, limit int, tag string) (int, int, string, bool) { for pos := start; pos < limit; { idx := strings.IndexByte(content[pos:limit], '<') if idx < 0 { return 0, 0, "", false } tagStart := pos + idx next := skipASCIISpaces(content, tagStart+1) if next < limit && content[next] == '/' { pos = tagStart + 1 continue } if !matchASCIIFoldAt(content, next, tag) { pos = tagStart + 1 continue } cursor := skipASCIISpaces(content, next+len(tag)) if cursor >= limit || content[cursor] != '=' { pos = tagStart + 1 continue } cursor = skipASCIISpaces(content, cursor+1) nameStart := cursor for cursor < limit && isTextToolIdentifierChar(content[cursor], cursor == nameStart) { cursor++ } if cursor == nameStart { pos = tagStart + 1 continue } name := content[nameStart:cursor] cursor = skipASCIISpaces(content, cursor) if cursor >= limit || content[cursor] != '>' { pos = tagStart + 1 continue } return tagStart, cursor + 1, name, true } return 0, 0, "", false } func findXMLCloseTag(content string, start, limit int, tag string) (int, int, bool) { for pos := start; pos < limit; { idx := strings.Index(content[pos:limit], "= limit || content[cursor] != '>' { pos = tagStart + 2 continue } return tagStart, cursor + 1, true } return 0, 0, false } func matchASCIIFoldAt(s string, pos int, want string) bool { if pos < 0 || pos+len(want) > len(s) { return false } for i := 0; i < len(want); i++ { a := s[pos+i] b := want[i] if a >= 'A' && a <= 'Z' { a += 'a' - 'A' } if b >= 'A' && b <= 'Z' { b += 'a' - 'A' } if a != b { return false } } return true } func findMustacheToolCallEnd(content string, argsStart int) (int, int, bool) { depth := 0 var quote byte escaped := false for i := argsStart; i < len(content); i++ { c := content[i] if quote != 0 { if escaped { escaped = false continue } if c == '\\' { escaped = true continue } if c == quote { quote = 0 } continue } switch c { case '\'', '"': quote = c case '[', '{', '(': depth++ case ']', '}': if depth > 0 { depth-- } case ')': if depth > 0 { depth-- continue } closeStart := skipASCIISpaces(content, i+1) if closeStart+1 < len(content) && content[closeStart] == '}' && content[closeStart+1] == '}' { return i, closeStart + 2, true } } } return 0, 0, false }