diff --git a/examples/server/parsers/bailing_parser.hpp b/examples/server/parsers/bailing_parser.hpp index 31bf6e10..ecdf100c 100644 --- a/examples/server/parsers/bailing_parser.hpp +++ b/examples/server/parsers/bailing_parser.hpp @@ -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): +// 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"([\s\S]*?)"); + 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 diff --git a/examples/server/server-context.cpp b/examples/server/server-context.cpp index 40a8e119..a9897dce 100644 --- a/examples/server/server-context.cpp +++ b/examples/server/server-context.cpp @@ -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); diff --git a/examples/server/test-bailing-parser.cpp b/examples/server/test-bailing-parser.cpp index d835da28..f40c9046 100644 --- a/examples/server/test-bailing-parser.cpp +++ b/examples/server/test-bailing-parser.cpp @@ -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("") == 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 web_search\nq"; + CHECK(bailing::remove_executed_blocks(partial) == partial); + if (failures == 0) std::cout << "ALL BAILING PARSER CHECKS PASSED\n"; return failures; }