<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{"city": "Tokyo"}<|tool▁call▁end|><|tool▁calls▁end|>
| | | | | | | |
0 19 38 47 58 76 92 110
Unicode strings have variable per character byte widths. | for example is 3 bytes: 357 275 234, while <
is only 1 byte. The C++ regex library counts bytes, not characters, so builder.pos() will show these numbers:
<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{"city": "Tokyo"}<|tool▁call▁end|><|tool▁calls▁end|>
| | | | | | | |
0 27 54 63 80 98 122 148
If you want to generate a number line like this for your own input string, see: https://github.com/createthis/ts_string_number_line
(?:<|tool▁call▁begin|>)?([^\\n<]+)(?:<|tool▁sep|>)https://regex101.com/r/LzjVI5/1
(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)https://regex101.com/r/d6svPr/1
(?:[\\s]*)?<|tool▁call▁end|>https://regex101.com/r/58yviQ/1
<|tool▁calls▁end|>https://regex101.com/r/6XJXLJ/1
Parsing input with format DeepSeek V3.1: <|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{"city": "Tokyo"}<|tool▁call▁end|><|tool▁calls▁end|>
common_chat_parse_deepseek_v3_1: thinking_forced_open: 0, pos=0
common_chat_parse_deepseek_v3_1: pos=0, start_pos=0
common_chat_parse_deepseek_v3_1: pos=0
common_chat_parse_deepseek_v3_1: try_parse_reasoning failed, pos=0
common_chat_parse_deepseek_v3_1: no thinking_forced_open, adding content
common_chat_parse_deepseek_v3_1_content: parse_tool_calls
common_chat_parse_deepseek_v3_1_content: pos=0
parse_json_tool_calls: block_open matched, pos=28
operator(): input = <|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{"city": "Tokyo"}<|tool▁call▁end|><|tool▁calls▁end|>
operator(): while first = 1, from = 18446744073709551615, pos=28
operator(): after function_regex, pos=81
operator(): function_regex matched, pos=81
operator(): group 1 name=get_time
operator(): from=npos
operator(): detected '{' pos=81,from=18446744073709551615
operator(): arguments exist, pos=98
operator(): consumed close_regex, pos=123
operator(): continue
operator(): while first = 0, from = 18446744073709551615, pos=123
operator(): after function_regex, pos=149
operator(): break
operator(): attempting to consume block_close, pos=149
Partial parse: <|tool▁calls▁end|>
Parsed message: {"role":"assistant","content":"<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": \"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>"}
simple
Expected: 1
Actual: 0
diff --git a/common/chat.cpp b/common/chat.cpp
index 0461450f..498e7d07 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -699,61 +699,83 @@ static void parse_json_tool_calls(
auto parse_tool_calls = [&]() {
size_t from = std::string::npos;
auto first = true;
+ LOG_ERR("%s: input = %s\n", __func__, builder.input().c_str());
+
while (true) {
+ LOG_ERR("%s: while first = %d, from = %lu, pos=%lu\n", __func__, first, from, builder.pos());
+ auto start_pos = builder.pos();
auto res = function_regex_start_only && first
? builder.try_consume_regex(*function_regex_start_only)
: function_regex
? builder.try_find_regex(*function_regex, from)
: std::nullopt;
+ LOG_ERR("%s: after function_regex, pos=%lu\n", __func__, builder.pos());
if (res) {
+ LOG_ERR("%s: function_regex matched, pos=%lu\n", __func__, builder.pos());
std::string name;
if (get_function_name) {
name = get_function_name(*res);
+ LOG_ERR("%s: get_function_name name=%s\n", __func__, name.c_str());
} else {
GGML_ASSERT(res->groups.size() == 2);
name = builder.str(res->groups[1]);
+ LOG_ERR("%s: group 1 name=%s\n", __func__, name.c_str());
}
first = false;
if (name.empty()) {
// get_function_name signalled us that we should skip this match and treat it as content.
from = res->groups[0].begin + 1;
+ LOG_ERR("%s: name empty, continue\n", __func__);
continue;
}
if (update_cursor) {
builder.move_to(res->groups[0].end);
from = builder.pos();
+ LOG_ERR("%s: update_cursor move_to=%lu,from=%lu\n", __func__, (unsigned long)res->groups[0].end,from);
} else {
from = std::string::npos;
+ LOG_ERR("%s: from=npos\n", __func__);
}
auto maybe_raw_python = name == "python" && allow_raw_python;
if (builder.input()[builder.pos()] == '{' || !maybe_raw_python) {
+ LOG_ERR("%s: detected '{' pos=%lu,from=%lu\n", __func__, builder.pos(),from);
if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
+ LOG_ERR("%s: arguments exist, pos=%lu\n", __func__, builder.pos());
if (!builder.add_tool_call(name, "", arguments->value) || arguments->is_partial) {
+ LOG_ERR("%s: not add_tool_call or is_partial=%d throwing incomplete tool call\n", __func__, arguments->is_partial);
throw common_chat_msg_partial_exception("incomplete tool call");
}
builder.consume_regex(close_regex);
+ LOG_ERR("%s: consumed close_regex, pos=%lu\n", __func__, builder.pos());
if (update_cursor) {
from = builder.pos(); // continue after this call
+ LOG_ERR("%s: update_cursor from=%lu\n", __func__, builder.pos());
continue;
}
}
if (update_cursor) {
+ LOG_ERR("%s: update_cursor throwing incomplete tool call\n", __func__);
throw common_chat_msg_partial_exception("incomplete tool call");
} else {
+ LOG_ERR("%s: continue\n", __func__);
continue;
}
}
if (maybe_raw_python) {
auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
if (!builder.add_tool_call(name, "", arguments)) {
+ LOG_ERR("%s: maybe_raw_python throwing incomplete tool call\n", __func__);
throw common_chat_msg_partial_exception("incomplete tool call");
}
+ LOG_ERR("%s: maybe_raw_python return\n", __func__);
return;
}
+ LOG_ERR("%s: throwing incomplete tool call\n", __func__);
throw common_chat_msg_partial_exception("incomplete tool call");
}
+ LOG_ERR("%s: break\n", __func__);
break;
}
if (block_close) {
@@ -761,16 +783,21 @@ static void parse_json_tool_calls(
// ensure we’re right after the last call header/close
if (from != std::string::npos) builder.move_to(from);
}
+ LOG_ERR("%s: attempting to consume block_close, pos=%lu\n", __func__, builder.pos());
builder.consume_regex(*block_close);
}
+ LOG_ERR("%s: consume_spaces, pos=%lu\n", __func__, builder.pos());
builder.consume_spaces();
+ LOG_ERR("%s: add_content, pos=%lu\n", __func__, builder.pos());
builder.add_content(builder.consume_rest());
};
if (block_open) {
if (auto res = builder.try_find_regex(*block_open)) {
+ LOG_DBG("%s: block_open matched, pos=%lu\n", __func__, builder.pos());
if (update_cursor) builder.move_to(res->groups[0].end); // consume opener
parse_tool_calls();
} else {
+ LOG_DBG("%s: block_open match failed, pos=%lu\n", __func__, builder.pos());
builder.add_content(builder.consume_rest());
}
} else {
@@ -1509,6 +1536,7 @@ static void common_chat_parse_deepseek_v3_1_content(common_chat_msg_parser & bui
}
LOG_DBG("%s: parse_tool_calls\n", __func__);
+ LOG_DBG("%s: pos=%lu\n", __func__, builder.pos());
parse_json_tool_calls(
builder,
@@ -1516,20 +1544,19 @@ static void common_chat_parse_deepseek_v3_1_content(common_chat_msg_parser & bui
/* function_regex_start_only= */ std::nullopt,
function_regex,
close_regex,
- tool_calls_end,
- false,
- nullptr,
- true);
+ tool_calls_end);
}
static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) {
// DeepSeek V3.1 outputs reasoning content between "<think>" and "</think>" tags, followed by regular content
// First try to parse using the standard reasoning parsing method
- LOG_DBG("%s: thinking_forced_open: %s\n", __func__, std::to_string(builder.syntax().thinking_forced_open).c_str());
+ LOG_DBG("%s: thinking_forced_open: %s, pos=%lu\n", __func__, std::to_string(builder.syntax().thinking_forced_open).c_str(), builder.pos());
auto start_pos = builder.pos();
auto found_end_think = builder.try_find_literal("</think>");
+ LOG_DBG("%s: pos=%lu, start_pos=%lu\n", __func__, builder.pos(), start_pos);
builder.move_to(start_pos);
+ LOG_DBG("%s: pos=%lu\n", __func__, builder.pos());
if (builder.syntax().thinking_forced_open && !builder.is_partial() && !found_end_think) {
LOG_DBG("%s: no end_think, not partial, adding content\n", __func__);
@@ -1540,6 +1567,7 @@ static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) {
// </think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>NAME\n```json\nJSON\n```<|tool▁call▁end|><|tool▁calls▁end|>
common_chat_parse_deepseek_v3_1_content(builder);
} else {
+ LOG_DBG("%s: try_parse_reasoning failed, pos=%lu\n", __func__, builder.pos());
if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE) {
LOG_DBG("%s: reasoning_format none, adding content\n", __func__);
common_chat_parse_deepseek_v3_1_content(builder);
diff --git a/tests/test-chat-parser.cpp b/tests/test-chat-parser.cpp
index 547ebb48..286324b5 100644
--- a/tests/test-chat-parser.cpp
+++ b/tests/test-chat-parser.cpp
@@ -52,7 +52,7 @@ static void assert_throws(const std::function<void()> & fn, const std::string &
}
static void test_reasoning() {
- //common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
+ common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
{
common_chat_msg_parser builder("<tnk>Cogito</tnk>Ergo sum", /* is_partial= */ false, {
/* .format = */ COMMON_CHAT_FORMAT_CONTENT_ONLY,
@@ -225,7 +225,8 @@ static void test(const std::string & input, bool is_partial, const std::vector<s
}
static void test_deepseek_v3_1_tool_calls() {
- //common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
+ common_log_set_verbosity_thold(LOG_DEFAULT_DEBUG);
+ LOG_DBG("%s: ==================== start simple\n", __func__);
// variant: happy path for when it works as the model card says it should
const std::string variant("simple");
common_chat_syntax syntax = {
@@ -243,6 +244,7 @@ static void test_deepseek_v3_1_tool_calls() {
assert_equals(variant, std::string("{\"city\":\"Tokyo\"}"), msg.tool_calls[0].arguments);
assert_equals(variant, std::string(""), msg.content);
assert_equals(variant, std::string(""), msg.reasoning_content);
+ LOG_DBG("%s: ==================== end simple\n", __func__);
// variant: simple + thinking open
{