Skip to content

Commit fb615a2

Browse files
committed
common: track think block and tool call status in chat parser
1 parent 86a3f0f commit fb615a2

File tree

5 files changed

+61
-0
lines changed

5 files changed

+61
-0
lines changed

common/chat-parser-xml-toolcall.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,9 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
705705

706706
// Parse content
707707
bool reasoning_unclosed = builder.syntax().thinking_forced_open;
708+
if (reasoning_unclosed) {
709+
builder.mark_reasoning_active(end_think);
710+
}
708711
std::string unclosed_reasoning_content("");
709712
for (;;) {
710713
auto tc = try_find_2_literal_splited_by_spaces(builder, form.scope_start, form.tool_start);
@@ -730,6 +733,7 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
730733
}
731734
} else {
732735
reasoning_unclosed = false;
736+
builder.mark_reasoning_closed();
733737
std::string reasoning_content;
734738
if (pos == std::string::npos) {
735739
reasoning_content = std::move(content);
@@ -766,13 +770,15 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
766770
bool toolcall_in_think = false;
767771
for (auto think_start = content.find(start_think); think_start != std::string::npos; think_start = content.find(start_think, think_start)) {
768772
if (auto think_end = content.find(end_think, think_start + start_think.size()); think_end != std::string::npos) {
773+
builder.mark_reasoning_active(end_think);
769774
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content) {
770775
auto reasoning_content = content.substr(think_start + start_think.size(), think_end - think_start - start_think.size());
771776
builder.add_reasoning_content(reasoning_content);
772777
think_start = erase_spaces(content, think_start, think_end + end_think.size() - 1);
773778
} else {
774779
think_start = think_end + end_think.size() - 1;
775780
}
781+
builder.mark_reasoning_closed();
776782
} else {
777783
// This <tool_call> start is in thinking block, skip this tool call
778784
// This <tool_call> start is in thinking block
@@ -782,6 +788,7 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
782788
unclosed_reasoning_content = content.substr(think_start + start_think.size()) + tool_call_start;
783789
}
784790
reasoning_unclosed = true;
791+
builder.mark_reasoning_active(end_think);
785792
content.resize(think_start);
786793
toolcall_in_think = true;
787794
}

common/chat-parser.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ void common_chat_msg_parser::add_reasoning_content(const std::string &reasoning_
156156
result_.reasoning_content += reasoning_content;
157157
}
158158

159+
void common_chat_msg_parser::mark_reasoning_active(const std::string & end_tag) {
160+
result_.reasoning_status.detected = true;
161+
result_.reasoning_status.active = true;
162+
if (!end_tag.empty()) {
163+
result_.reasoning_status.end_tag = end_tag;
164+
}
165+
}
166+
167+
void common_chat_msg_parser::mark_reasoning_closed() {
168+
if (result_.reasoning_status.detected) {
169+
result_.reasoning_status.active = false;
170+
}
171+
}
172+
159173
bool common_chat_msg_parser::add_tool_call(const std::string & name, const std::string & id, const std::string & arguments) {
160174
if (name.empty()) {
161175
return false;
@@ -329,11 +343,13 @@ bool common_chat_msg_parser::try_parse_reasoning(const std::string & start_think
329343
const size_t saved_pos = pos_;
330344
const size_t saved_content_size = result_.content.size();
331345
const size_t saved_reasoning_size = result_.reasoning_content.size();
346+
const auto saved_reasoning_status = result_.reasoning_status;
332347

333348
auto restore_state = [&]() {
334349
move_to(saved_pos);
335350
result_.content.resize(saved_content_size);
336351
result_.reasoning_content.resize(saved_reasoning_size);
352+
result_.reasoning_status = saved_reasoning_status;
337353
};
338354

339355
// Allow leading whitespace to be preserved as content when reasoning is present at the start
@@ -370,9 +386,11 @@ bool common_chat_msg_parser::try_parse_reasoning(const std::string & start_think
370386
if (whitespace_end > pos_) {
371387
add_content(input_.substr(pos_, whitespace_end - pos_));
372388
}
389+
mark_reasoning_active(end_think);
373390
set_reasoning_prefix(cursor);
374391
cursor += start_think.size();
375392
} else if (syntax_.thinking_forced_open) {
393+
mark_reasoning_active(end_think);
376394
cursor = whitespace_end;
377395
} else {
378396
restore_state();
@@ -398,8 +416,10 @@ bool common_chat_msg_parser::try_parse_reasoning(const std::string & start_think
398416

399417
if (end_pos > cursor) {
400418
handle_reasoning(input_.substr(cursor, end_pos - cursor), /* closed */ true);
419+
mark_reasoning_closed();
401420
} else {
402421
handle_reasoning("", /* closed */ true);
422+
mark_reasoning_closed();
403423
}
404424

405425
cursor = end_pos + end_think.size();
@@ -420,6 +440,7 @@ bool common_chat_msg_parser::try_parse_reasoning(const std::string & start_think
420440
move_to(input_.size());
421441
return true;
422442
}
443+
mark_reasoning_active(end_think);
423444
set_reasoning_prefix(cursor);
424445
cursor += start_think.size();
425446
continue;
@@ -1492,17 +1513,24 @@ common_chat_msg common_chat_parse(const std::string & input, bool is_partial, co
14921513
return common_chat_peg_parse(syntax.parser, input, is_partial, syntax);
14931514
}
14941515
common_chat_msg_parser builder(input, is_partial, syntax);
1516+
bool partial_exception_caught = false;
14951517
try {
14961518
common_chat_parse(builder);
14971519
} catch (const common_chat_msg_partial_exception & ex) {
14981520
LOG_DBG("Partial parse: %s\n", ex.what());
1521+
partial_exception_caught = true;
14991522
if (!is_partial) {
15001523
builder.clear_tools();
15011524
builder.move_to(0);
15021525
common_chat_parse_content_only(builder);
15031526
}
15041527
}
15051528
auto msg = builder.result();
1529+
// Mark tool_call_in_progress if we caught a partial exception during partial parsing
1530+
// and there are tool calls in progress (indicates incomplete tool call parsing)
1531+
if (is_partial && partial_exception_caught && !msg.tool_calls.empty()) {
1532+
msg.tool_call_in_progress = true;
1533+
}
15061534
if (!is_partial) {
15071535
LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat<json>({msg}).at(0).dump().c_str());
15081536
}

common/chat-parser.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class common_chat_msg_parser {
5656
// Appends to the result.reasoning_content field
5757
void add_reasoning_content(const std::string & reasoning_content);
5858

59+
// Track reasoning status to expose start/end markers to callers
60+
void mark_reasoning_active(const std::string & end_tag);
61+
void mark_reasoning_closed();
62+
5963
// Adds a tool call to the result. If the tool call is too incomplete (e.g. name empty), it won't add anything.
6064
bool add_tool_call(const std::string & name, const std::string & id, const std::string & arguments);
6165

common/chat.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ struct common_chat_tool_call {
2222
}
2323
};
2424

25+
struct common_chat_reasoning_status {
26+
bool detected = false; // a reasoning block start was observed
27+
bool active = false; // we are currently inside a reasoning block (not closed yet)
28+
std::string end_tag; // closing tag to use when forcing a close
29+
30+
bool operator==(const common_chat_reasoning_status & other) const {
31+
return detected == other.detected && active == other.active && end_tag == other.end_tag;
32+
}
33+
bool operator!=(const common_chat_reasoning_status & other) const {
34+
return !(*this == other);
35+
}
36+
};
37+
2538
struct common_chat_msg_content_part {
2639
std::string type;
2740
std::string text;
@@ -37,6 +50,8 @@ struct common_chat_msg {
3750
std::vector<common_chat_msg_content_part> content_parts;
3851
std::vector<common_chat_tool_call> tool_calls;
3952
std::string reasoning_content;
53+
common_chat_reasoning_status reasoning_status;
54+
bool tool_call_in_progress = false;
4055
std::string tool_name;
4156
std::string tool_call_id;
4257

@@ -63,6 +78,7 @@ struct common_chat_msg {
6378
&& content_parts == other.content_parts
6479
&& tool_calls == other.tool_calls
6580
&& reasoning_content == other.reasoning_content
81+
&& reasoning_status == other.reasoning_status
6682
&& tool_name == other.tool_name
6783
&& tool_call_id == other.tool_call_id;
6884
}

tests/test-chat-parser.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ static void test_reasoning() {
119119
auto msg = common_chat_parse(input, false, syntax);
120120
assert_equals(variant, std::string("Pense"), msg.reasoning_content);
121121
assert_equals(variant, std::string("Bonjour"), msg.content);
122+
assert_equals(variant, true, msg.reasoning_status.detected);
123+
assert_equals(variant, false, msg.reasoning_status.active);
124+
assert_equals(variant, std::string("</think>"), msg.reasoning_status.end_tag);
122125
}
123126
{
124127
const std::string variant("llama_3_inline_think");
@@ -133,6 +136,9 @@ static void test_reasoning() {
133136
auto msg = common_chat_parse(input, false, syntax);
134137
assert_equals(variant, std::string("Plan"), msg.reasoning_content);
135138
assert_equals(variant, std::string("Réponse"), msg.content);
139+
assert_equals(variant, true, msg.reasoning_status.detected);
140+
assert_equals(variant, false, msg.reasoning_status.active);
141+
assert_equals(variant, std::string("</think>"), msg.reasoning_status.end_tag);
136142
}
137143
// Test DeepSeek V3.1 parsing - reasoning content followed by "</think>" and then regular content
138144
{

0 commit comments

Comments
 (0)