feat(server): strip executed Bailing blocks from reasoning_content

vLLM Ling3 parity: reasoning ends where the executed call begins, so
downstream panels never render converted calls as thinking text. Only
complete blocks are removed; truncated fragments and narration survive.
Separate commit on top of the rescue so each reverts independently.
This commit is contained in:
Marvin 2026-09-07 21:44:54 -03:00
parent 86bc7658c5
commit 253cf76f59
3 changed files with 38 additions and 1 deletions

View File

@ -24,7 +24,9 @@ using json = nlohmann::ordered_json;
// The PEG layer consumes the thinking region as reasoning_content before the
// tool stage runs, so the call is lost: the response carries empty content,
// no structured tool calls, and the call text parked in reasoning_content.
// This parser recovers well-formed blocks from that region. It mirrors the
// Reference semantic is vLLM's Ling3 parser (vllm/parser/ling3.py): <tool_call>
// acts as an implicit reasoning terminator, thinking ends where the call
// begins. This parser recovers well-formed blocks from that region. It mirrors the
// behavior validated client-side (Hermes reasoning_tool_rescue.py, 330
// rescued turns over a 2h probe with no loop breakage):
// - only complete open-to-close blocks; truncated fragments are skipped,
@ -119,4 +121,24 @@ static bool has_complete_block(const std::string & text) {
}
}
// Remove executed (complete) blocks from reasoning text. Mirrors the vLLM
// Ling3 terminator semantic: reasoning ends where the call begins, so panels
// downstream never display converted calls. Only complete blocks are removed;
// truncated fragments are left untouched. Leading/trailing whitespace left by
// removal is trimmed; internal formatting is preserved.
static std::string remove_executed_blocks(const std::string & text) {
try {
std::regex block_regex(R"(<tool_call>[\s\S]*?</tool_call>)");
std::string cleaned = std::regex_replace(text, block_regex, "");
const char * ws = " \t\n\r";
cleaned.erase(0, cleaned.find_first_not_of(ws));
if (!cleaned.empty()) {
cleaned.erase(cleaned.find_last_not_of(ws) + 1);
}
return cleaned;
} catch (const std::exception &) {
return text;
}
}
} // namespace bailing

View File

@ -749,6 +749,11 @@ const common_chat_msg& server_slot::update_chat_msg(bool is_partial, std::vector
if (!rescued.empty() && !new_msg.tool_calls.empty()) {
LLAMA_LOG_WARN("Bailing rescue: promoted %d tool call(s) trapped in reasoning_content\n",
(int) new_msg.tool_calls.size());
// Terminator display semantic (vLLM Ling3 parity): reasoning ends
// where the executed call begins. Strip converted blocks so
// downstream panels never render them as thinking text.
new_msg.reasoning_content =
bailing::remove_executed_blocks(new_msg.reasoning_content);
}
}
//new_msg.ensure_tool_call_ids_set(generated_tool_call_ids, gen_tool_call_id);

View File

@ -46,6 +46,16 @@ int main() {
CHECK(bailing::parse_tool_calls("Just thinking aloud here.").empty());
CHECK(bailing::parse_tool_calls("").empty());
// 7. Strip removes only executed (complete) blocks, preserves narration.
std::string mixed = "Let me search for that now.\n" + trapped + "\nDone thinking.";
std::string stripped = bailing::remove_executed_blocks(mixed);
CHECK(stripped.find("<tool_call>") == std::string::npos);
CHECK(stripped.find("Let me search for that now.") != std::string::npos);
CHECK(stripped.find("Done thinking.") != std::string::npos);
// Truncated fragments survive the strip.
std::string partial = "thinking <tool_call>web_search\n<arg_key>q";
CHECK(bailing::remove_executed_blocks(partial) == partial);
if (failures == 0) std::cout << "ALL BAILING PARSER CHECKS PASSED\n";
return failures;
}