diff --git a/server/src/server/tool_parser.cpp b/server/src/server/tool_parser.cpp index 6765507cd..390a96808 100644 --- a/server/src/server/tool_parser.cpp +++ b/server/src/server/tool_parser.cpp @@ -67,14 +67,17 @@ static const char TOOL_CODE_OPEN[] = ""; static const char ATTRIBUTE_PARAMETER_OPEN[] = ""; +// Parameter names may start with '-': Claude Code's Grep takes -i, -n, -A, -B +// and -C, and a call whose names no pattern matches is dropped whole. +// // Where an unclosed attribute-style parameter value ends: at a line break that // is followed by a complete sibling open tag carrying only name/string // attributes. Anything looser fires on literal tag text inside verbatim values // (echo '', or XML content such as ). #define SIBLING_PARAM_BOUNDARY \ R"(\n(?=\s*<(?:|DSML|)?(?:param|parameter)\s+)" \ - R"((?:name\s*=\s*["']?[A-Za-z_][\w.\-]*["']?(?:\s+string\s*=\s*["']?[^\s"'>]+["']?)?)" \ - R"(|string\s*=\s*["']?[^\s"'>]+["']?\s+name\s*=\s*["']?[A-Za-z_][\w.\-]*["']?)\s*>))" + R"((?:name\s*=\s*["']?-?[A-Za-z_][\w.\-]*["']?(?:\s+string\s*=\s*["']?[^\s"'>]+["']?)?)" \ + R"(|string\s*=\s*["']?[^\s"'>]+["']?\s+name\s*=\s*["']?-?[A-Za-z_][\w.\-]*["']?)\s*>))" @@ -356,13 +359,13 @@ static const std::regex & re_bare_tool_name_xml() { static const std::regex & re_attribute_tool_xml() { static std::regex r( - R"re(([\s\S]*?))re"); + R"re(([\s\S]*?))re"); return r; } static const std::regex & re_attribute_parameter_xml() { static std::regex r( - R"re(([\s\S]*?))re"); + R"re(([\s\S]*?))re"); return r; } @@ -380,7 +383,7 @@ static const std::regex & re_space_function_xml() { static const std::regex & re_complete_parameter_xml() { static std::regex r( - R"(([\s\S]*?))"); + R"(([\s\S]*?))"); return r; } @@ -731,8 +734,8 @@ static bool parse_xml_tool_call_body(const std::string & body, const json & tool // 3. Extract parameter key-value pairs: // a. Attribute style: <(param|parameter) name="key" string="true|false">value or value static const std::regex re_attr_param( - R"(<(?:|DSML|)?(?:param|parameter)\s+(?:name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?\s+string\s*=\s*["']?true["']?|string\s*=\s*["']?true["']?\s+name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?)\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))" - R"(|<(?:|DSML|)?(?:param|parameter)\s+name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?(?:\s+string\s*=\s*["']?([^\s"'>]+)["']?)?\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S]))|([\s\S]*?))"); + R"(<(?:|DSML|)?(?:param|parameter)\s+(?:name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?\s+string\s*=\s*["']?true["']?|string\s*=\s*["']?true["']?\s+name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?)\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))" + R"(|<(?:|DSML|)?(?:param|parameter)\s+name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?(?:\s+string\s*=\s*["']?([^\s"'>]+)["']?)?\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S]))|([\s\S]*?))"); auto pbegin = std::sregex_iterator(trimmed_params.begin(), trimmed_params.end(), re_attr_param); auto pend = std::sregex_iterator(); if (pbegin != pend) { @@ -1562,8 +1565,8 @@ ToolParseResult parse_tool_calls(const std::string & raw_text, const json & tool R"(([\s\S]*?))" R"((?:(?=\s*(?:<(?:|DSML|)?invoke[\s/>]||\{|(?![\s\S])))|(?=<(?:|DSML|)?invoke[\s/>])|(?=)|(?![\s\S])))"); static const std::regex re_param( - R"(<(?:|DSML|)?(param|parameter)\s+(?:name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?\s+string\s*=\s*["']?true["']?|string\s*=\s*["']?true["']?\s+name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?)\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))" - R"(|<(?:|DSML|)?(param|parameter)\s+name\s*=\s*["']?([A-Za-z_][\w.\-]*)["']?(?:\s+string\s*=\s*["']?([^\s"'>]+)["']?)?\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))"); + R"(<(?:|DSML|)?(param|parameter)\s+(?:name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?\s+string\s*=\s*["']?true["']?|string\s*=\s*["']?true["']?\s+name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?)\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))" + R"(|<(?:|DSML|)?(param|parameter)\s+name\s*=\s*["']?(-?[A-Za-z_][\w.\-]*)["']?(?:\s+string\s*=\s*["']?([^\s"'>]+)["']?)?\s*>([\s\S]*?)(?:|)" SIBLING_PARAM_BOUNDARY R"(|(?![\s\S])))"); auto fbegin = std::sregex_iterator(text.begin(), text.end(), re_block); auto fend = std::sregex_iterator(); diff --git a/server/test/unit/test_server_unit.cpp b/server/test/unit/test_server_unit.cpp index 6e765595f..e1aa19ee8 100644 --- a/server/test/unit/test_server_unit.cpp +++ b/server/test/unit/test_server_unit.cpp @@ -1574,6 +1574,44 @@ TEST_CASE(ServerUnitFixture, test_parse_dsml_tool_calls_multiple_parameters) { TEST_ASSERT(result.cleaned_text.empty()); } +TEST_CASE(ServerUnitFixture, test_parse_dsml_tool_calls_dash_parameter_names) { + // DeepSeek V4.1 calling Claude Code's Grep, whose flags are named -i and -n. + const std::string text = + "I'll explore the repository.\n\n" + "<|DSML|tool_calls>\n\n" + "<|DSML|invoke name=\"Grep\">\n" + "<|DSML|parameter name=\"pattern\" string=\"true\">prefix.?cache|PrefixCache\n" + "<|DSML|parameter name=\"output_mode\" string=\"true\">files_with_matches\n" + "<|DSML|parameter name=\"-i\" string=\"false\">true\n" + "<|DSML|parameter name=\"-n\" string=\"false\">true\n" + "\n" + ""; + json tools = json::array({ + {{"type", "function"}, {"function", { + {"name", "Grep"}, + {"parameters", { + {"type", "object"}, + {"properties", { + {"pattern", {{"type", "string"}}}, + {"output_mode", {{"type", "string"}}}, + {"-i", {{"type", "boolean"}}}, + {"-n", {{"type", "boolean"}}} + }} + }} + }}} + }); + auto result = parse_tool_calls(text, tools); + TEST_ASSERT(result.tool_calls.size() == 1); + if (!result.tool_calls.empty()) { + TEST_ASSERT(result.tool_calls[0].name == "Grep"); + auto args = json::parse(result.tool_calls[0].arguments); + TEST_ASSERT(args["pattern"] == "prefix.?cache|PrefixCache"); + TEST_ASSERT(args["-i"] == true); + TEST_ASSERT(args["-n"] == true); + } + TEST_ASSERT(result.cleaned_text == "I'll explore the repository."); +} + TEST_CASE(ServerUnitFixture, test_parse_dsml_tool_calls_edit_tool) { const std::string text = "<|DSML|tool_calls>\n"