package openai import ( "encoding/json" "strconv" "strings" ) func parseMustacheToolCallArguments(raw string) (map[string]any, bool) { if args, ok := parseMustacheToolCallArgumentsStrict(raw); ok { return args, true } // Some Cline/provider outputs include one extra "}" before the closing // template parens, e.g. `...True}]})}}`. Recover only after strict parsing. trimmed := strings.TrimSpace(raw) if strings.HasSuffix(trimmed, "}") { return parseMustacheToolCallArgumentsStrict(strings.TrimSpace(trimmed[:len(trimmed)-1])) } return nil, false } func parseMustacheToolCallArgumentsStrict(raw string) (map[string]any, bool) { trimmed := strings.TrimSpace(raw) args := map[string]any{} if trimmed == "" { return args, true } for _, part := range splitTopLevel(trimmed, ',') { part = strings.TrimSpace(part) if part == "" { continue } eq := indexTopLevel(part, '=') if eq < 0 { return nil, false } key := strings.TrimSpace(part[:eq]) if !isTextToolIdentifier(key) { return nil, false } value, ok := parseTextToolLiteral(part[eq+1:]) if !ok { return nil, false } args[key] = value } return args, true } func parseTextToolLiteral(raw string) (any, bool) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return "", true } var decoded any if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil { return decoded, true } parser := pythonishLiteralParser{s: trimmed} value, ok := parser.parseValue() if !ok { return nil, false } parser.skipSpaces() if parser.pos != len(parser.s) { return nil, false } return value, true } func splitTopLevel(s string, sep byte) []string { parts := []string{} start := 0 depth := 0 var quote byte escaped := false for i := 0; i < len(s); i++ { c := s[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 sep: if depth == 0 { parts = append(parts, s[start:i]) start = i + 1 } } } parts = append(parts, s[start:]) return parts } func indexTopLevel(s string, target byte) int { depth := 0 var quote byte escaped := false for i := 0; i < len(s); i++ { c := s[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-- } default: if c == target && depth == 0 { return i } } } return -1 } func skipASCIISpaces(s string, pos int) int { for pos < len(s) { switch s[pos] { case ' ', '\n', '\r', '\t': pos++ default: return pos } } return pos } type pythonishLiteralParser struct { s string pos int } func (p *pythonishLiteralParser) parseValue() (any, bool) { p.skipSpaces() if p.pos >= len(p.s) { return nil, false } switch p.s[p.pos] { case '\'', '"': return p.parseString() case '[': return p.parseArray() case '{': return p.parseObject() } if p.consumeWord("True") || p.consumeWord("true") { return true, true } if p.consumeWord("False") || p.consumeWord("false") { return false, true } if p.consumeWord("None") || p.consumeWord("null") { return nil, true } return p.parseNumberOrBare() } func (p *pythonishLiteralParser) parseString() (string, bool) { if p.pos >= len(p.s) { return "", false } quote := p.s[p.pos] p.pos++ var b strings.Builder for p.pos < len(p.s) { c := p.s[p.pos] p.pos++ if c == quote { return b.String(), true } if c != '\\' { b.WriteByte(c) continue } if p.pos >= len(p.s) { return "", false } esc := p.s[p.pos] p.pos++ switch esc { case '\\', '\'', '"': b.WriteByte(esc) case 'n': b.WriteByte('\n') case 'r': b.WriteByte('\r') case 't': b.WriteByte('\t') case 'b': b.WriteByte('\b') case 'f': b.WriteByte('\f') case 'u': if p.pos+4 > len(p.s) { return "", false } r, err := strconv.ParseInt(p.s[p.pos:p.pos+4], 16, 32) if err != nil { return "", false } b.WriteRune(rune(r)) p.pos += 4 default: b.WriteByte(esc) } } return "", false } func (p *pythonishLiteralParser) parseArray() ([]any, bool) { p.pos++ items := []any{} p.skipSpaces() if p.consumeByte(']') { return items, true } for { value, ok := p.parseValue() if !ok { return nil, false } items = append(items, value) p.skipSpaces() if p.consumeByte(']') { return items, true } if !p.consumeByte(',') { return nil, false } p.skipSpaces() if p.consumeByte(']') { return items, true } } } func (p *pythonishLiteralParser) parseObject() (map[string]any, bool) { p.pos++ obj := map[string]any{} p.skipSpaces() if p.consumeByte('}') { return obj, true } for { key, ok := p.parseObjectKey() if !ok { return nil, false } p.skipSpaces() if !p.consumeByte(':') { return nil, false } value, ok := p.parseValue() if !ok { return nil, false } obj[key] = value p.skipSpaces() if p.consumeByte('}') { return obj, true } if !p.consumeByte(',') { return nil, false } p.skipSpaces() if p.consumeByte('}') { return obj, true } } } func (p *pythonishLiteralParser) parseObjectKey() (string, bool) { p.skipSpaces() if p.pos >= len(p.s) { return "", false } if p.s[p.pos] == '\'' || p.s[p.pos] == '"' { return p.parseString() } start := p.pos for p.pos < len(p.s) && isTextToolIdentifierChar(p.s[p.pos], p.pos == start) { p.pos++ } if start == p.pos { return "", false } return p.s[start:p.pos], true } func (p *pythonishLiteralParser) parseNumberOrBare() (any, bool) { start := p.pos for p.pos < len(p.s) && !isLiteralDelimiter(p.s[p.pos]) { p.pos++ } if start == p.pos { return nil, false } token := p.s[start:p.pos] if value, ok := parseNumberToken(token); ok { return value, true } if isTextToolIdentifier(token) { return token, true } return nil, false } func (p *pythonishLiteralParser) skipSpaces() { for p.pos < len(p.s) { switch p.s[p.pos] { case ' ', '\n', '\r', '\t': p.pos++ default: return } } } func (p *pythonishLiteralParser) consumeByte(b byte) bool { if p.pos < len(p.s) && p.s[p.pos] == b { p.pos++ return true } return false } func (p *pythonishLiteralParser) consumeWord(word string) bool { if !strings.HasPrefix(p.s[p.pos:], word) { return false } end := p.pos + len(word) if end < len(p.s) && isBareLiteralChar(p.s[end]) { return false } p.pos = end return true } func parseNumberToken(token string) (any, bool) { if token == "" { return nil, false } if strings.ContainsAny(token, ".eE") { value, err := strconv.ParseFloat(token, 64) if err != nil { return nil, false } return value, true } value, err := strconv.ParseInt(token, 10, 64) if err != nil { return nil, false } return value, true } func isTextToolIdentifier(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { if !isTextToolIdentifierChar(s[i], i == 0) { return false } } return true } func isTextToolIdentifierChar(c byte, first bool) bool { if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' { return true } if !first && (c >= '0' && c <= '9' || c == '.' || c == ':' || c == '-') { return true } return false } func isBareLiteralChar(c byte) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' } func isLiteralDelimiter(c byte) bool { switch c { case ',', ']', '}', ' ', '\n', '\r', '\t': return true default: return false } } func mapKeys(m map[string]any) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys }